Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396e61cdb3 | ||
|
|
dfaab8c386 | ||
|
|
f2f5a06be1 | ||
|
|
8d7ff4d7db | ||
|
|
9f052bdf8b | ||
|
|
5472c8513f | ||
|
|
c028ec9083 | ||
|
|
31a87935a5 | ||
|
|
9e20ee35e1 | ||
|
|
0d4ef18358 | ||
|
|
8bde80a3d2 | ||
|
|
020f561ad4 | ||
|
|
432cc2003e | ||
|
|
aea8a6d04b | ||
|
|
e449f0bda4 | ||
|
|
ff3cb6c5cc | ||
|
|
2b4f7ab56f | ||
|
|
f5a8216be6 | ||
|
|
3779b4a923 | ||
|
|
9738e4a225 | ||
|
|
0905016b1f | ||
|
|
5f8ecfaf81 | ||
|
|
8da46afab4 | ||
|
|
facf4684ae | ||
|
|
2624897efe | ||
|
|
df6f53a161 | ||
|
|
b941551fff | ||
|
|
471775ae49 | ||
|
|
3d84bdf77b | ||
|
|
8668143127 | ||
|
|
295bad59bb | ||
|
|
804ee3b298 | ||
|
|
75c07221ef | ||
|
|
b82794df05 | ||
|
|
167656b38e | ||
|
|
5d81f875cb | ||
|
|
6ae200e338 | ||
|
|
ab6b902fb5 | ||
|
|
9f423b01ef | ||
|
|
c863c86f4c | ||
|
|
2bd27a5d0b | ||
|
|
cff8f88920 | ||
|
|
87f5479662 | ||
|
|
a589705e6d | ||
|
|
ee062c13d4 | ||
|
|
30645bc4e0 | ||
|
|
0dd07d10a0 | ||
|
|
1fd166d5c7 | ||
|
|
96599df89f | ||
|
|
2ec13c64f3 | ||
|
|
c916eeb9d7 | ||
|
|
8ee85a4007 | ||
|
|
1aa716de55 | ||
|
|
f631bdc782 | ||
|
|
81cb055375 | ||
|
|
b4a42602e2 | ||
|
|
57171f57e4 | ||
|
|
82df434d19 | ||
|
|
264deab637 | ||
|
|
69345ed26c | ||
|
|
36c0be1097 | ||
|
|
e12bc6aa19 | ||
|
|
64d4d64aa7 | ||
|
|
757f1484e9 | ||
|
|
7979608cc5 | ||
|
|
1918f7f0aa | ||
|
|
ea0c9c65d9 | ||
|
|
8aec85c579 | ||
|
|
4fa03f4938 | ||
|
|
e0a957c4e9 | ||
|
|
5db72e5fee | ||
|
|
8ce2fff8ab | ||
|
|
5a24e30820 | ||
|
|
b78efdd155 | ||
|
|
dab18e5b40 | ||
|
|
66c8f8d8df | ||
|
|
dd282963c3 | ||
|
|
d023a81a32 | ||
|
|
73e8f955ca | ||
|
|
5e7657fc40 | ||
|
|
76b4d4c10c | ||
|
|
b3c975314d | ||
|
|
4e7e6e57fe | ||
|
|
0b78d3173d | ||
|
|
92d7e5c58a | ||
|
|
632d013fb8 | ||
|
|
207894dac6 | ||
|
|
6f170b1ad7 | ||
|
|
6dbe25fcc5 | ||
|
|
74828adcb8 | ||
|
|
3120cd54fe | ||
|
|
b1cafc06eb | ||
|
|
fd66fb33a8 | ||
|
|
5af9d0164b | ||
|
|
049a01d58f | ||
|
|
629af0efc3 | ||
|
|
a1262c2406 | ||
|
|
97dd879597 | ||
|
|
f1321d6140 | ||
|
|
0646f48ca6 | ||
|
|
a50d65393e | ||
|
|
67221b015d | ||
|
|
40aadbad85 | ||
|
|
77ebf306a3 | ||
|
|
94d3924432 | ||
|
|
1235ea5bb5 | ||
|
|
321ed12663 | ||
|
|
265af01f9c | ||
|
|
a9961df4e2 | ||
|
|
8d3f35f4f7 | ||
|
|
2b8ae406a3 | ||
|
|
d78f1a3ff9 | ||
|
|
c500c9c199 | ||
|
|
b2363d2783 | ||
|
|
8aba600fa5 | ||
|
|
18596ecc34 | ||
|
|
420d289d35 | ||
|
|
eebd0f113b | ||
|
|
c4286984ab | ||
|
|
e0d6a0b974 | ||
|
|
71e46860ac | ||
|
|
ce942ffe16 | ||
|
|
e083ef0d6d | ||
|
|
b91fb3f586 | ||
|
|
79356baee1 | ||
|
|
cb6c0b6e45 | ||
|
|
543bc24bfd | ||
|
|
789ff72081 | ||
|
|
5dc4754181 | ||
|
|
eaa64b636a | ||
|
|
1c9cd40d34 | ||
|
|
9c54181ff8 | ||
|
|
d4211441b3 | ||
|
|
3307debacc | ||
|
|
95fd6ecab1 | ||
|
|
84dca41008 | ||
|
|
b3d90f04ac | ||
|
|
c2550dbca9 | ||
|
|
fe11ed3ac7 | ||
|
|
24b5eb3405 | ||
|
|
bc16c49187 | ||
|
|
3438e0bfb0 | ||
|
|
7e9abd2350 | ||
|
|
8f6880d809 | ||
|
|
e0024e59f3 | ||
|
|
b9b604c007 | ||
|
|
be6c30fb0d | ||
|
|
7001543d28 | ||
|
|
bc38c08a5e | ||
|
|
7f49ebb4ec | ||
|
|
3746d2935b | ||
|
|
7b6577d543 | ||
|
|
f6643ebc12 | ||
|
|
fd9ab2704c | ||
|
|
f241003ac6 | ||
|
|
38f7843861 | ||
|
|
25e95ae1a6 | ||
|
|
4c1c5e56ab | ||
|
|
ed29b675ee | ||
|
|
3d501ceaf9 | ||
|
|
c5b2c8c680 | ||
|
|
f29fe22d3d | ||
|
|
2540a0396d | ||
|
|
9fec3f35ff | ||
|
|
679b075ecc | ||
|
|
b1819d4766 | ||
|
|
96b7053884 | ||
|
|
fcbf71dad7 | ||
|
|
aee791a17d | ||
|
|
5b2fe66903 | ||
|
|
f4daa4508f | ||
|
|
755155479a | ||
|
|
978118a400 | ||
|
|
4a91da60dd | ||
|
|
db9ca80b69 | ||
|
|
e147a41f92 | ||
|
|
497f871447 | ||
|
|
ad860afb8b | ||
|
|
b4933a5645 | ||
|
|
46f437126c | ||
|
|
90b85f2956 | ||
|
|
ebfbf7cc8e | ||
|
|
499ac76c43 | ||
|
|
fd7f83378d | ||
|
|
e7b575badc | ||
|
|
a0f2d81337 | ||
|
|
fb6980a81e | ||
|
|
df45459618 | ||
|
|
61b2d92595 | ||
|
|
adda27ec57 | ||
|
|
b92b5b37fb | ||
|
|
18d36e1b30 | ||
|
|
f4cb447f0a | ||
|
|
069617eba0 | ||
|
|
aff193a003 | ||
|
|
eb6a86a009 | ||
|
|
97025fe8ef | ||
|
|
08bb0103e8 | ||
|
|
e02789c70c | ||
|
|
cf7a451198 | ||
|
|
f088498f26 | ||
|
|
bcc20e0aec | ||
|
|
e236214fd5 | ||
|
|
b103caf9d4 | ||
|
|
a43a4aea5e | ||
|
|
9155c49571 | ||
|
|
baa15110ff | ||
|
|
0234041e1e | ||
|
|
2fb7523d06 | ||
|
|
95e087390f | ||
|
|
0821b8a25f | ||
|
|
2b823556b3 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
|||||||
github: [binwiederhier]
|
github: [binwiederhier]
|
||||||
|
liberapay: ntfy
|
||||||
|
|||||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.18.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
|||||||
36
.github/workflows/docs.yaml
vendored
Normal file
36
.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
publish-docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout ntfy code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
-
|
||||||
|
name: Checkout docs pages code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: binwiederhier/ntfy-docs.github.io
|
||||||
|
path: build/ntfy-docs.github.io
|
||||||
|
token: ${{secrets.NTFY_DOCS_PUSH_TOKEN}}
|
||||||
|
# Expires after 1 year, re-generate via
|
||||||
|
# User -> Settings -> Developer options -> Personal Access Tokens -> Fine Grained Token
|
||||||
|
-
|
||||||
|
name: Build docs
|
||||||
|
run: make docs
|
||||||
|
-
|
||||||
|
name: Copy generated docs
|
||||||
|
run: rsync -av --exclude CNAME --delete server/docs/ build/ntfy-docs.github.io/docs/
|
||||||
|
-
|
||||||
|
name: Publish docs
|
||||||
|
run: |
|
||||||
|
cd build/ntfy-docs.github.io
|
||||||
|
git config user.name "GitHub Actions Bot"
|
||||||
|
git config user.email "<>"
|
||||||
|
git add docs/
|
||||||
|
git commit -m "Updated docs"
|
||||||
|
git push origin main
|
||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.18.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
|||||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
|||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.18.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
|||||||
28
.gitpod.yml
Normal file
28
.gitpod.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
tasks:
|
||||||
|
- name: docs
|
||||||
|
before: make docs-deps
|
||||||
|
command: mkdocs serve
|
||||||
|
- name: binary
|
||||||
|
before: |
|
||||||
|
npm install --global nodemon
|
||||||
|
make cli-deps-static-sites
|
||||||
|
command: |
|
||||||
|
nodemon --watch './**/*.go' --ext go --signal SIGTERM --exec "CGO_ENABLED=1 go run main.go serve --listen-http :2586 --debug --base-url $(gp url 2586)"
|
||||||
|
openMode: split-right
|
||||||
|
- name: web
|
||||||
|
before: make web-deps
|
||||||
|
command: cd web && npm start
|
||||||
|
openMode: split-right
|
||||||
|
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- golang.go
|
||||||
|
- ms-azuretools.vscode-docker
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- name: docs
|
||||||
|
port: 8000
|
||||||
|
- name: binary
|
||||||
|
port: 2586
|
||||||
|
- name: web
|
||||||
|
port: 3000
|
||||||
@@ -13,9 +13,6 @@ builds:
|
|||||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [amd64]
|
goarch: [amd64]
|
||||||
hooks:
|
|
||||||
post:
|
|
||||||
- upx "{{ .Path }}" # apt install upx
|
|
||||||
-
|
-
|
||||||
id: ntfy_linux_armv6
|
id: ntfy_linux_armv6
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
@@ -28,7 +25,6 @@ builds:
|
|||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm]
|
goarch: [arm]
|
||||||
goarm: [6]
|
goarm: [6]
|
||||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
|
||||||
-
|
-
|
||||||
id: ntfy_linux_armv7
|
id: ntfy_linux_armv7
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
@@ -41,7 +37,6 @@ builds:
|
|||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm]
|
goarch: [arm]
|
||||||
goarm: [7]
|
goarm: [7]
|
||||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
|
||||||
-
|
-
|
||||||
id: ntfy_linux_arm64
|
id: ntfy_linux_arm64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
@@ -53,7 +48,6 @@ builds:
|
|||||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm64]
|
goarch: [arm64]
|
||||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
|
||||||
-
|
-
|
||||||
id: ntfy_windows_amd64
|
id: ntfy_windows_amd64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
@@ -64,7 +58,6 @@ builds:
|
|||||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [windows]
|
goos: [windows]
|
||||||
goarch: [amd64]
|
goarch: [amd64]
|
||||||
# No "upx" for Windows to hopefully avoid Virus warnings
|
|
||||||
-
|
-
|
||||||
id: ntfy_darwin_all
|
id: ntfy_darwin_all
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
|
|||||||
133
CODE_OF_CONDUCT.md
Normal file
133
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement via Discord/Matrix (binwiederhier),
|
||||||
|
or email (ntfy@heckel.io). All complaints will be reviewed and investigated promptly
|
||||||
|
and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|
||||||
@@ -3,5 +3,7 @@ MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
|
|||||||
|
|
||||||
COPY ntfy /usr/bin
|
COPY ntfy /usr/bin
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=60s --timeout=10s CMD wget -q --tries=1 http://localhost/v1/health -O - | grep -Eo '"healthy"\s*:\s*true' || exit 1
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
ENTRYPOINT ["ntfy"]
|
ENTRYPOINT ["ntfy"]
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -88,7 +88,6 @@ build-deps-ubuntu:
|
|||||||
curl \
|
curl \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
upx \
|
|
||||||
jq
|
jq
|
||||||
which pip3 || sudo apt install -y python3-pip
|
which pip3 || sudo apt install -y python3-pip
|
||||||
|
|
||||||
@@ -201,7 +200,6 @@ cli-deps-static-sites:
|
|||||||
touch server/docs/index.html server/site/app.html
|
touch server/docs/index.html server/site/app.html
|
||||||
|
|
||||||
cli-deps-all:
|
cli-deps-all:
|
||||||
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
|
|
||||||
go install github.com/goreleaser/goreleaser@latest
|
go install github.com/goreleaser/goreleaser@latest
|
||||||
|
|
||||||
cli-deps-gcc-armv6-armv7:
|
cli-deps-gcc-armv6-armv7:
|
||||||
|
|||||||
63
README.md
63
README.md
@@ -1,13 +1,5 @@
|
|||||||

|

|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 👶 Baby break - My baby girl was born!
|
|
||||||
Hey folks, my daughter was born on 8/30/22, so I'll be taking some time off from working on ntfy. I'll likely return
|
|
||||||
to working on features and bugs in a few weeks. I hope you understand. I posted some pictures in [#387](https://github.com/binwiederhier/ntfy/issues/387) 🥰
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 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://github.com/binwiederhier/ntfy/releases/latest)
|
||||||
[](https://pkg.go.dev/heckel.io/ntfy)
|
[](https://pkg.go.dev/heckel.io/ntfy)
|
||||||
@@ -19,6 +11,7 @@ to working on features and bugs in a few weeks. I hope you understand. I posted
|
|||||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||||
[](https://www.reddit.com/r/ntfy/)
|
[](https://www.reddit.com/r/ntfy/)
|
||||||
[](https://ntfy.statuspage.io/)
|
[](https://ntfy.statuspage.io/)
|
||||||
|
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||||
|
|
||||||
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
|
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
|
||||||
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
|
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
|
||||||
@@ -58,19 +51,19 @@ topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.a
|
|||||||
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out
|
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||||
the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app.
|
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||||
Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
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/).
|
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||||
|
|
||||||
<a href="https://hosted.weblate.org/engage/ntfy/">
|
<a href="https://hosted.weblate.org/engage/ntfy/">
|
||||||
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Donations
|
## Sponsors
|
||||||
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
|
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
||||||
appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
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/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/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||||
@@ -96,6 +89,46 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
|||||||
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.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/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/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>
|
||||||
|
|
||||||
|
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
|
## License
|
||||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ var cmdPublish = &cli.Command{
|
|||||||
Usage: "Send message via a ntfy server",
|
Usage: "Send message via a ntfy server",
|
||||||
UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
|
UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
|
||||||
ntfy publish [OPTIONS..] --wait-cmd COMMAND...
|
ntfy publish [OPTIONS..] --wait-cmd COMMAND...
|
||||||
NTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE...]`,
|
NTFY_TOPIC=.. ntfy publish [OPTIONS..] [MESSAGE...]`,
|
||||||
Action: execPublish,
|
Action: execPublish,
|
||||||
Category: categoryClient,
|
Category: categoryClient,
|
||||||
Flags: flagsPublish,
|
Flags: flagsPublish,
|
||||||
@@ -72,7 +72,7 @@ Examples:
|
|||||||
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
|
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
|
||||||
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
|
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
|
||||||
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
|
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
|
||||||
NTFY_TOPIC=mytopic ntfy pub -P "some message" # Use NTFY_TOPIC variable as topic
|
NTFY_TOPIC=mytopic ntfy pub "some message" # Use NTFY_TOPIC variable as topic
|
||||||
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
|
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
|
||||||
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
||||||
|
|
||||||
@@ -241,13 +241,9 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
|
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
|
||||||
envTopic := c.Bool("env-topic")
|
envTopic := os.Getenv("NTFY_TOPIC")
|
||||||
if envTopic {
|
if envTopic != "" {
|
||||||
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mDeprecation notice: The --env-topic/-P flag will be removed in July 2022, see https://ntfy.sh/docs/deprecations/ for details.\x1b[0m")
|
topic = envTopic
|
||||||
topic = os.Getenv("NTFY_TOPIC")
|
|
||||||
if topic == "" {
|
|
||||||
return "", nil, errors.New("when --env-topic is passed, must define NTFY_TOPIC environment variable")
|
|
||||||
}
|
|
||||||
return topic, remainingArgs(c, 0), nil
|
return topic, remainingArgs(c, 0), nil
|
||||||
}
|
}
|
||||||
if c.NArg() < 1 {
|
if c.NArg() < 1 {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
|||||||
|
|
||||||
app, _, _, _ := newTestApp()
|
app, _, _, _ := newTestApp()
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
||||||
|
time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
|
||||||
|
|
||||||
app2, _, stdout, _ := newTestApp()
|
app2, _, stdout, _ := newTestApp()
|
||||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ var flagsServe = append(
|
|||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
||||||
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
|
||||||
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||||
@@ -110,6 +112,8 @@ func execServe(c *cli.Context) error {
|
|||||||
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")
|
||||||
|
cacheBatchSize := c.Int("cache-batch-size")
|
||||||
|
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||||
authFile := c.String("auth-file")
|
authFile := c.String("auth-file")
|
||||||
authDefaultAccess := c.String("auth-default-access")
|
authDefaultAccess := c.String("auth-default-access")
|
||||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||||
@@ -233,6 +237,8 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.CacheFile = cacheFile
|
conf.CacheFile = cacheFile
|
||||||
conf.CacheDuration = cacheDuration
|
conf.CacheDuration = cacheDuration
|
||||||
conf.CacheStartupQueries = cacheStartupQueries
|
conf.CacheStartupQueries = cacheStartupQueries
|
||||||
|
conf.CacheBatchSize = cacheBatchSize
|
||||||
|
conf.CacheBatchTimeout = cacheBatchTimeout
|
||||||
conf.AuthFile = authFile
|
conf.AuthFile = authFile
|
||||||
conf.AuthDefaultRead = authDefaultRead
|
conf.AuthDefaultRead = authDefaultRead
|
||||||
conf.AuthDefaultWrite = authDefaultWrite
|
conf.AuthDefaultWrite = authDefaultWrite
|
||||||
|
|||||||
@@ -309,6 +309,25 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
|
|||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example: UnifiedPush
|
||||||
|
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
|
||||||
|
has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.
|
||||||
|
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the
|
||||||
|
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
|
||||||
|
|
||||||
|
To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either
|
||||||
|
allow anonymous write access for the entire prefix or explicitly per topic:
|
||||||
|
|
||||||
|
=== "Prefix"
|
||||||
|
```
|
||||||
|
$ ntfy access '*' 'up*' write-only
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Explicitly"
|
||||||
|
```
|
||||||
|
$ ntfy access '*' upYzMtZGZiYTY5 write-only
|
||||||
|
```
|
||||||
|
|
||||||
## E-mail notifications
|
## E-mail notifications
|
||||||
To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,
|
To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,
|
||||||
you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.
|
you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.
|
||||||
@@ -448,7 +467,8 @@ or the root domain:
|
|||||||
# This config allows insecure HTTP POST/PUT requests against topics to allow a short curl syntax (without -L
|
# This config allows insecure HTTP POST/PUT requests against topics to allow a short curl syntax (without -L
|
||||||
# and "https://" prefix). It also disables output buffering, which has worked well for the ntfy.sh server.
|
# and "https://" prefix). It also disables output buffering, which has worked well for the ntfy.sh server.
|
||||||
#
|
#
|
||||||
# This is how ntfy.sh is configured.
|
# This is pretty much how ntfy.sh is configured. To see the exact configuration,
|
||||||
|
# see https://github.com/binwiederhier/ntfy-ansible/
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
@@ -489,14 +509,17 @@ or the root domain:
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
server_name ntfy.sh;
|
server_name ntfy.sh;
|
||||||
|
|
||||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||||
|
ssl_session_tickets off;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
||||||
|
|
||||||
@@ -553,13 +576,16 @@ or the root domain:
|
|||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl http2;
|
||||||
server_name ntfy.sh;
|
server_name ntfy.sh;
|
||||||
|
|
||||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||||
|
ssl_session_tickets off;
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
||||||
@@ -806,19 +832,27 @@ out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/
|
|||||||
|
|
||||||
Depending on *how you run it*, here are a few limits that are relevant:
|
Depending on *how you run it*, here are a few limits that are relevant:
|
||||||
|
|
||||||
### WAL for message cache
|
### Message cache
|
||||||
By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it
|
By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it
|
||||||
syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh,
|
syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh,
|
||||||
the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted.
|
the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted.
|
||||||
See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details.
|
See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details.
|
||||||
|
|
||||||
|
In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache
|
||||||
|
in batches, and asynchronously. This can be enabled with the `cache-batch-size` and `cache-batch-timeout`. If you start
|
||||||
|
seeing `database locked` messages in the logs, you should probably enable that.
|
||||||
|
|
||||||
Here's how ntfy.sh has been tuned in the `server.yml` file:
|
Here's how ntfy.sh has been tuned in the `server.yml` file:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
|
cache-batch-size: 25
|
||||||
|
cache-batch-timeout: "1s"
|
||||||
cache-startup-queries: |
|
cache-startup-queries: |
|
||||||
pragma journal_mode = WAL;
|
pragma journal_mode = WAL;
|
||||||
pragma synchronous = normal;
|
pragma synchronous = normal;
|
||||||
pragma temp_store = memory;
|
pragma temp_store = memory;
|
||||||
|
pragma busy_timeout = 15000;
|
||||||
|
vacuum;
|
||||||
```
|
```
|
||||||
|
|
||||||
### For systemd services
|
### For systemd services
|
||||||
@@ -880,7 +914,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
|||||||
```
|
```
|
||||||
# Rate limit all IP addresses
|
# Rate limit all IP addresses
|
||||||
http {
|
http {
|
||||||
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
|
limit_req_zone $binary_remote_addr zone=one:10m rate=45r/m;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Alternatively, whitelist certain IP addresses
|
# Alternatively, whitelist certain IP addresses
|
||||||
@@ -895,7 +929,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
|||||||
1 $binary_remote_addr;
|
1 $binary_remote_addr;
|
||||||
0 "";
|
0 "";
|
||||||
}
|
}
|
||||||
limit_req_zone $limitkey zone=one:10m rate=1r/s;
|
limit_req_zone $limitkey zone=one:10m rate=45r/m;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -924,7 +958,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
|||||||
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
|
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
|
||||||
logpath = /var/log/nginx/error.log
|
logpath = /var/log/nginx/error.log
|
||||||
findtime = 600
|
findtime = 600
|
||||||
bantime = 7200
|
bantime = 14400
|
||||||
maxretry = 10
|
maxretry = 10
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -971,6 +1005,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||||||
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
||||||
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
|
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
|
||||||
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) |
|
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) |
|
||||||
|
| `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) |
|
||||||
|
| `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) |
|
||||||
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
|
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
|
||||||
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
|
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
|
||||||
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
|
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
|
||||||
@@ -1035,6 +1071,8 @@ OPTIONS:
|
|||||||
--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]
|
||||||
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
||||||
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
||||||
|
--cache-batch-size value, --cache_batch_size value max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
|
||||||
|
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]
|
||||||
--cache-startup-queries value, --cache_startup_queries value queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
|
--cache-startup-queries value, --cache_startup_queries value queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
|
||||||
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
|
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_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]
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ This page is used to list deprecation notices for ntfy. Deprecated commands and
|
|||||||
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
|
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
|
||||||
|
|
||||||
## Active deprecations
|
## Active deprecations
|
||||||
|
_No active deprecations_
|
||||||
|
|
||||||
|
## Previous deprecations
|
||||||
|
|
||||||
### ntfy CLI: `ntfy publish --env-topic` will be removed
|
### ntfy CLI: `ntfy publish --env-topic` will be removed
|
||||||
> Active since 2022-06-20, behavior will change end of **July 2022**
|
> Active since 2022-06-20, behavior changed with v1.30.1
|
||||||
|
|
||||||
The `ntfy publish --env-topic` option will be removed. It'll still be possible to specify a topic via the
|
The `ntfy publish --env-topic` option will be removed. It'll still be possible to specify a topic via the
|
||||||
`NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.
|
`NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.
|
||||||
|
|
||||||
=== "Before"
|
=== "Before"
|
||||||
@@ -21,8 +24,6 @@ The `ntfy publish --env-topic` option will be removed. It'll still be possible t
|
|||||||
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"
|
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Previous deprecations
|
|
||||||
|
|
||||||
### <del>Android app: WebSockets will become the default connection protocol</del>
|
### <del>Android app: WebSockets will become the default connection protocol</del>
|
||||||
> Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)
|
> Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ Build related:
|
|||||||
The `web/` and `docs/` folder are the sources for web app and documentation. During the build process,
|
The `web/` and `docs/` folder are the sources for web app and documentation. During the build process,
|
||||||
the generated output is copied to `server/site` (web app and landing page) and `server/docs` (documentation).
|
the generated output is copied to `server/site` (web app and landing page) and `server/docs` (documentation).
|
||||||
|
|
||||||
|
### Build/test on Gitpod
|
||||||
|
To get a quick working development environment you can use [Gitpod](https://gitpod.io), an in-browser IDE
|
||||||
|
that makes it easy to develop ntfy without having to set up a desktop IDE. For any real development,
|
||||||
|
I do suggest a proper IDE like [IntelliJ IDEA](https://www.jetbrains.com/idea/).
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||||
|
|
||||||
### Build requirements
|
### Build requirements
|
||||||
|
|
||||||
* [Go](https://go.dev/) (required for main server)
|
* [Go](https://go.dev/) (required for main server)
|
||||||
@@ -85,7 +92,6 @@ sudo apt install \
|
|||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
upx \
|
|
||||||
git
|
git
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -321,7 +327,76 @@ To build your own version with Firebase, you must:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## iOS app
|
## iOS app
|
||||||
The ntfy iOS app source code is available [on GitHub](https://github.com/binwiederhier/ntfy-ios).
|
Building the iOS app is very involved. Please report any inconsistencies or issues with it. The requirements are
|
||||||
|
strictly based off of my development on this app. There may be other versions of macOS / XCode that work.
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
1. macOS Monterey or later
|
||||||
|
1. XCode 13.2+
|
||||||
|
1. A physical iOS device (for push notifications, Firebase does not work in the XCode simulator)
|
||||||
|
1. Firebase account
|
||||||
|
1. Apple Developer license? (I forget if it's possible to do testing without purchasing the license)
|
||||||
|
|
||||||
|
### Apple setup
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
I haven't had time to move the build instructions here. Please check out the repository instead.
|
Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required
|
||||||
|
for these changes to take effect in the iOS app.
|
||||||
|
|
||||||
|
1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add)
|
||||||
|
1. Select "Apple Push Notifications service (APNs)"
|
||||||
|
1. Download the newly created key (should have a file name similar to `AuthKey_ZZZZZZ.p8`, where `ZZZZZZ` is the **Key ID**)
|
||||||
|
1. Record your **Team ID** - it can be seen in the top-right corner of the page, or on your Account > Membership page
|
||||||
|
1. Next, navigate to "Project Settings" in the firebase console for your project, and select the iOS app you created. Then, click "Cloud Messaging" in the left sidebar, and scroll down to the "APNs Authentication Key" section. Click "Upload Key", and upload the key you downloaded from Apple Developer.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
If you don't do the above setups for APNS, **notifications will not post instantly or sometimes at all**. This is because of the missing APNS key, which is required for firebase to send notifications to the iOS app. See below for a snip from the firebase docs.
|
||||||
|
|
||||||
|
If you don't have an APNs authentication key, you can still send notifications to iOS devices, but they won't be delivered
|
||||||
|
instantly. Instead, they'll be delivered when the device wakes up to check for new notifications or when your application
|
||||||
|
sends a firebase request to check for them. The time to check for new notifications can vary from a few seconds to hours,
|
||||||
|
days or even weeks. Enabling APNs authentication keys ensures that notifications are delivered instantly and is strongly
|
||||||
|
recommended.
|
||||||
|
|
||||||
|
### Firebase setup
|
||||||
|
|
||||||
|
1. If you haven't already, create a Google / Firebase account
|
||||||
|
1. Visit the [Firebase console](https://console.firebase.google.com)
|
||||||
|
1. Create a new Firebase project:
|
||||||
|
1. Enter a project name
|
||||||
|
1. Disable Google Analytics (currently iOS app does not support analytics)
|
||||||
|
1. On the "Project settings" page, add an iOS app
|
||||||
|
1. Apple bundle ID - "com.copephobia.ntfy-ios" (this can be changed to match XCode's ntfy.sh target > "Bundle Identifier" value)
|
||||||
|
1. Register the app
|
||||||
|
1. Download the config file - GoogleInfo.plist (this will need to be included in the ntfy-ios repository / XCode)
|
||||||
|
1. Generate a new service account private key for the ntfy server
|
||||||
|
1. Go to "Project settings" > "Service accounts"
|
||||||
|
1. Click "Generate new private key" to generate and download a private key to use for sending messages via the ntfy server
|
||||||
|
|
||||||
|
### ntfy server
|
||||||
|
Note that the ntfy server is not officially supported on macOS. It should, however, be able to run on macOS using these
|
||||||
|
steps:
|
||||||
|
|
||||||
|
1. If not already made, make the `/etc/ntfy/` directory and move the service account private key to that folder
|
||||||
|
1. Copy the `server/server.yml` file from the ntfy repository to `/etc/ntfy/`
|
||||||
|
1. Modify the `/etc/ntfy/server.yml` file `firebase-key-file` value to the path of the private key
|
||||||
|
1. Install go: `brew install go`
|
||||||
|
1. In the ntfy repository, run `make cli-darwin-server`.
|
||||||
|
|
||||||
|
### XCode setup
|
||||||
|
|
||||||
|
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) 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
|
||||||
|
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
|
||||||
|
|
||||||
|
### PLIST config
|
||||||
|
To have instant notifications/better notification delivery when using firebase, you will need to add the
|
||||||
|
`GoogleService-Info.plist` file to your project. Here's how to do that:
|
||||||
|
|
||||||
|
1. In XCode, find the NTFY app target. **Not** the NSE app target.
|
||||||
|
1. Find the Asset/ folder in the project navigator
|
||||||
|
1. Drag the `GoogleService-Info.plist` file into the Asset/ folder that you get from the firebase console. It can be
|
||||||
|
found in the "Project settings" > "General" > "Your apps" with a button labled "GoogleService-Info.plist"
|
||||||
|
|
||||||
|
After that, you should be all set!
|
||||||
|
|||||||
@@ -122,6 +122,19 @@ to ntfy at its default URL (`attrs` and other attributes are optional):
|
|||||||
priority: 1
|
priority: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## GitHub Actions
|
||||||
|
You can send a message during a workflow run with curl. Here is an example sending info about the repo, commit and job status.
|
||||||
|
``` yaml
|
||||||
|
- name: Actions Ntfy
|
||||||
|
run: |
|
||||||
|
curl \
|
||||||
|
-u ${{ secrets.NTFY_CRED }} \
|
||||||
|
-H "Title: Title here" \
|
||||||
|
-H "Content-Type: text/plain" \
|
||||||
|
-d $'Repo: ${{ github.repository }}\nCommit: ${{ github.sha }}\nRef: ${{ github.ref }}\nStatus: ${{ job.status}}' \
|
||||||
|
${{ secrets.NTFY_URL }}
|
||||||
|
```
|
||||||
|
|
||||||
## Watchtower (shoutrrr)
|
## Watchtower (shoutrrr)
|
||||||
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
||||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||||
@@ -400,7 +413,8 @@ alerting:
|
|||||||
|
|
||||||
## Jellyseerr/Overseerr webhook
|
## Jellyseerr/Overseerr webhook
|
||||||
Here is an example for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) webhook
|
Here is an example for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) webhook
|
||||||
JSON payload. Remember to change the `https://requests.example.com` to your jellyseerr/overseerr URL.
|
JSON payload. Remember to change the `https://request.example.com` to your URL as the value of the JSON key click.
|
||||||
|
And if you're not using the request `topic`, make sure to change it in the JSON payload to your topic.
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
{
|
{
|
||||||
|
|||||||
24
docs/faq.md
24
docs/faq.md
@@ -4,11 +4,20 @@
|
|||||||
Who knows. I didn't do a lot of research before making this. It was fun making it.
|
Who knows. I didn't do a lot of research before making this. It was fun making it.
|
||||||
|
|
||||||
## Can I use this in my app? Will it stay free?
|
## Can I use this in my app? Will it stay free?
|
||||||
Yes. As long as you don't abuse it, it'll be available and free of charge. I do not plan on monetizing
|
Yes. As long as you don't abuse it, it'll be available and free of charge. While I will always allow usage of the ntfy.sh
|
||||||
the service.
|
server without signup and free of charge, I may also offer paid plans in the future.
|
||||||
|
|
||||||
## What are the uptime guarantees?
|
## What are the uptime guarantees?
|
||||||
Best effort.
|
Best effort.
|
||||||
|
|
||||||
|
ntfy currently runs on a single DigitalOcean droplet, without any scale out strategy or redundancies. When the time comes,
|
||||||
|
I'll add scale out features, but for now it is what it is.
|
||||||
|
|
||||||
|
In the first year of its life, and to this day (Dec'22), ntfy had **no outages** that I can remember. Other than short
|
||||||
|
blips and some HTTP 500 spikes, it has been rock solid.
|
||||||
|
|
||||||
|
There is a [status page](https://ntfy.statuspage.io/) which is updated based on some automated checks via the amazingly
|
||||||
|
awesome [healthchecks.io](https://healthchecks.io/) (_no affiliation, just a fan_).
|
||||||
|
|
||||||
## What happens if there are multiple subscribers to the same topic?
|
## What happens if there are multiple subscribers to the same topic?
|
||||||
As per usual with pub-sub, all subscribers receive notifications if they are subscribed to a topic.
|
As per usual with pub-sub, all subscribers receive notifications if they are subscribed to a topic.
|
||||||
@@ -43,6 +52,15 @@ decent now.
|
|||||||
server and listens for incoming notifications. This consumes additional battery (see above),
|
server and listens for incoming notifications. This consumes additional battery (see above),
|
||||||
but delivers notifications instantly.
|
but delivers notifications instantly.
|
||||||
|
|
||||||
|
## Can you implement feature X?
|
||||||
|
Yes, maybe. Check out [existing GitHub issues](https://github.com/binwiederhier/ntfy/issues) to see if somebody else had
|
||||||
|
the same idea before you, or file a new issue. I'll likely get back to you within a few days.
|
||||||
|
|
||||||
|
## I'm having issues with iOS, can you help? The iOS app is behind compared to the Android app, can you fix that?
|
||||||
|
The iOS is very bare bones and quite frankly a little buggy. I wanted to get something out the door to make the iOS users
|
||||||
|
happy, but halfway through I got frustrated with iOS development and paused development. I will eventually get back to
|
||||||
|
it, or hopefully, somebody else will come along and help out. Please review the [known issues](known-issues.md) for details.
|
||||||
|
|
||||||
## Can I disable the web app? Can I protect it with a login screen?
|
## Can I disable the web app? Can I protect it with a login screen?
|
||||||
The web app is a static website without a backend (other than the ntfy API). All data is stored locally in the browser
|
The web app is a static website without a backend (other than the ntfy API). All data is stored locally in the browser
|
||||||
cache and local storage. That means it does not need to be protected with a login screen, and it poses no additional
|
cache and local storage. That means it does not need to be protected with a login screen, and it poses no additional
|
||||||
|
|||||||
227
docs/install.md
227
docs/install.md
@@ -14,7 +14,7 @@ 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` (or `/etc/ntfy/client.yml`, 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) 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)
|
||||||
@@ -26,37 +26,37 @@ deb/rpm packages.
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_x86_64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_x86_64.tar.gz
|
||||||
tar zxvf ntfy_1.29.0_linux_x86_64.tar.gz
|
tar zxvf ntfy_1.31.0_linux_x86_64.tar.gz
|
||||||
sudo cp -a ntfy_1.29.0_linux_x86_64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.31.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_1.29.0_linux_armv6.tar.gz
|
tar zxvf ntfy_1.31.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_1.29.0_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.31.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.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/v1.29.0/ntfy_1.29.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_1.29.0_linux_armv7.tar.gz
|
tar zxvf ntfy_1.31.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_1.29.0_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.31.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.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/v1.29.0/ntfy_1.29.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_1.29.0_linux_arm64.tar.gz
|
tar zxvf ntfy_1.31.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_1.29.0_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.31.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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
|
||||||
@@ -114,7 +114,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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
|
||||||
@@ -122,7 +122,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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
|
||||||
@@ -130,7 +130,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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
|
||||||
@@ -140,28 +140,28 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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/v1.29.0/ntfy_1.29.0_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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/v1.29.0/ntfy_1.29.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.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/v1.29.0/ntfy_1.29.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
@@ -189,18 +189,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/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz),
|
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_macOS_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/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz > ntfy_1.29.0_macOS_all.tar.gz
|
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_macOS_all.tar.gz > ntfy_1.31.0_macOS_all.tar.gz
|
||||||
tar zxvf ntfy_1.29.0_macOS_all.tar.gz
|
tar zxvf ntfy_1.31.0_macOS_all.tar.gz
|
||||||
sudo cp -a ntfy_1.29.0_macOS_all/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_1.31.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||||
mkdir ~/Library/Application\ Support/ntfy
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
cp ntfy_1.29.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
cp ntfy_1.31.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
ntfy --help
|
ntfy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ ntfy --help
|
|||||||
|
|
||||||
## 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/v1.29.0/ntfy_1.29.0_windows_x86_64.zip),
|
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_windows_x86_64.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).
|
||||||
@@ -287,7 +287,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files to the same uid/gid.
|
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
|
||||||
|
|
||||||
Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately.
|
Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately.
|
||||||
```
|
```
|
||||||
@@ -300,8 +300,8 @@ This image can be pushed to a container registry and shipped independently. All
|
|||||||
## Kubernetes
|
## Kubernetes
|
||||||
|
|
||||||
The setup for Kubernetes is very similar to that for Docker, and requires a fairly minimal deployment or pod definition to function. There
|
The setup for Kubernetes is very similar to that for Docker, and requires a fairly minimal deployment or pod definition to function. There
|
||||||
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistant cache, and a standalone
|
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone
|
||||||
unmanaged pod.
|
unmanned pod.
|
||||||
|
|
||||||
|
|
||||||
=== "deployment"
|
=== "deployment"
|
||||||
@@ -371,7 +371,7 @@ unmanaged pod.
|
|||||||
containers:
|
containers:
|
||||||
- name: ntfy
|
- name: ntfy
|
||||||
image: binwiederhier/ntfy
|
image: binwiederhier/ntfy
|
||||||
args: ["serve", "--cache-file /var/cache/ntfy/cache.db"]
|
args: ["serve", "--cache-file", "/var/cache/ntfy/cache.db"]
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
name: http
|
name: http
|
||||||
@@ -379,6 +379,8 @@ unmanaged pod.
|
|||||||
- name: config
|
- name: config
|
||||||
mountPath: "/etc/ntfy"
|
mountPath: "/etc/ntfy"
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
- name: cache
|
||||||
|
mountPath: "/var/cache/ntfy"
|
||||||
volumes:
|
volumes:
|
||||||
- name: config
|
- name: config
|
||||||
configMap:
|
configMap:
|
||||||
@@ -422,7 +424,7 @@ unmanaged pod.
|
|||||||
name: ntfy
|
name: ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
Configuration is relatively straightforward. As an exmaple, a minimal configuration is provided.
|
Configuration is relatively straightforward. As an example, a minimal configuration is provided.
|
||||||
|
|
||||||
=== "resource definition"
|
=== "resource definition"
|
||||||
```yaml
|
```yaml
|
||||||
@@ -431,7 +433,7 @@ Configuration is relatively straightforward. As an exmaple, a minimal configurat
|
|||||||
metadata:
|
metadata:
|
||||||
name: ntfy
|
name: ntfy
|
||||||
data:
|
data:
|
||||||
server.yml: |
|
server.yml: |
|
||||||
# Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml
|
# Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml
|
||||||
base-url: https://ntfy.sh
|
base-url: https://ntfy.sh
|
||||||
```
|
```
|
||||||
@@ -440,3 +442,154 @@ Configuration is relatively straightforward. As an exmaple, a minimal configurat
|
|||||||
```bash
|
```bash
|
||||||
kubectl create configmap ntfy --from-file=server.yml
|
kubectl create configmap ntfy --from-file=server.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Kustomize
|
||||||
|
|
||||||
|
ntfy can be deployed in a Kubernetes cluster with [Kustomize](https://github.com/kubernetes-sigs/kustomize), a tool used
|
||||||
|
to customize Kubernetes objects using a `kustomization.yaml` file.
|
||||||
|
|
||||||
|
1. Create new folder - `ntfy`
|
||||||
|
2. Add all files listed below
|
||||||
|
1. `kustomization.yaml` - stores all configmaps and resources used in a deployment
|
||||||
|
2. `ntfy-deployment.yaml` - define deployment type and its parameters
|
||||||
|
3. `ntfy-pvc.yaml` - describes how [persistent volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) will be created
|
||||||
|
4. `ntfy-svc.yaml` - expose application to the internal kubernetes network
|
||||||
|
5. `ntfy-ingress.yaml` - expose service to outside the network using [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/)
|
||||||
|
6. `server.yaml` - simple server configuration
|
||||||
|
3. Replace **TESTNAMESPACE** within `kustomization.yaml` with designated namespace
|
||||||
|
4. Replace **ntfy.test** within `ntfy-ingress.yaml` with desired DNS name
|
||||||
|
5. Apply configuration to cluster set in current context:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -k /ntfy
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "kustomization.yaml"
|
||||||
|
```yaml
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ntfy-deployment.yaml # deployment definition
|
||||||
|
- ntfy-svc.yaml # service connecting pods to cluster network
|
||||||
|
- ntfy-pvc.yaml # pvc used to store cache and attachment
|
||||||
|
- ntfy-ingress.yaml # ingress definition
|
||||||
|
configMapGenerator: # will parse config from raw config to configmap,it allows for dynamic reload of application if additional app is deployed ie https://github.com/stakater/Reloader
|
||||||
|
- name: server-config
|
||||||
|
files:
|
||||||
|
- server.yml
|
||||||
|
namespace: TESTNAMESPACE # select namespace for whole application
|
||||||
|
```
|
||||||
|
=== "ntfy-deployment.yaml"
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: ntfy-deployment
|
||||||
|
labels:
|
||||||
|
app: ntfy-deployment
|
||||||
|
spec:
|
||||||
|
revisionHistoryLimit: 1
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ntfy-pod
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ntfy-pod
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: ntfy
|
||||||
|
image: binwiederhier/ntfy:v1.28.0 # set deployed version
|
||||||
|
args: ["serve"]
|
||||||
|
env: #example of adjustments made in environmental variables
|
||||||
|
- name: TZ # set timezone
|
||||||
|
value: XXXXXXX
|
||||||
|
- name: NTFY_DEBUG # enable/disable debug
|
||||||
|
value: "false"
|
||||||
|
- name: NTFY_LOG_LEVEL # adjust log level
|
||||||
|
value: INFO
|
||||||
|
- name: NTFY_BASE_URL # add base url
|
||||||
|
value: XXXXXXXXXX
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http-ntfy
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 300Mi
|
||||||
|
cpu: 200m
|
||||||
|
requests:
|
||||||
|
cpu: 150m
|
||||||
|
memory: 150Mi
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /etc/ntfy/server.yml
|
||||||
|
subPath: server.yml
|
||||||
|
name: config-volume # generated vie configMapGenerator from kustomization file
|
||||||
|
- mountPath: /var/cache/ntfy
|
||||||
|
name: cache-volume #cache volume mounted to persistent volume
|
||||||
|
volumes:
|
||||||
|
- name: config-volume
|
||||||
|
configMap: # uses configmap generator to parse server.yml to configmap
|
||||||
|
name: server-config
|
||||||
|
- name: cache-volume
|
||||||
|
persistentVolumeClaim: # stores /cache/ntfy in defined pv
|
||||||
|
claimName: ntfy-pvc
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy-pvc.yaml"
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: ntfy-pvc
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path # adjust storage if needed
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy-svc.yaml"
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: ntfy-svc
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: ntfy-pod
|
||||||
|
ports:
|
||||||
|
- name: http-ntfy-out
|
||||||
|
protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: http-ntfy
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy-ingress.yaml"
|
||||||
|
```yaml
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: ntfy-ingress
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: ntfy.test #select own
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ntfy-svc
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "server.yml"
|
||||||
|
```yaml
|
||||||
|
cache-file: "/var/cache/ntfy/cache.db"
|
||||||
|
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,26 +6,34 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||||||
|
|
||||||
## Public ntfy servers
|
## Public ntfy servers
|
||||||
|
|
||||||
| URL | Country |
|
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
|
||||||
|---------------------------------------------------|:---------:|
|
ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||||
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 |
|
|
||||||
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 🇪🇺 |
|
|
||||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 🇪🇺 |
|
|
||||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 🇪🇺 |
|
|
||||||
|
|
||||||
Thanks to everyone running a public server. **You guys rock!** To the users: Be aware that server operators can log your
|
| URL | Country |
|
||||||
messages until I finally finish implementing end-to-end encryption.
|
|---------------------------------------------------|--------------------|
|
||||||
|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
|
||||||
|
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
|
||||||
|
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
||||||
|
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||||
|
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
## Official integrations
|
## Official integrations
|
||||||
|
|
||||||
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push Notifications that work with just about every platform
|
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
|
||||||
|
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform
|
||||||
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
|
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
|
||||||
- [Robusta](https://docs.robusta.dev/master/catalog/sinks/webhook.html) ⭐ - open source platform for Kubernetes troubleshooting
|
- [Robusta](https://docs.robusta.dev/master/catalog/sinks/webhook.html) ⭐ - open source platform for Kubernetes troubleshooting
|
||||||
- [borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#third-party-monitoring-services) ⭐ - configuration-driven backup software for servers and workstations
|
- [borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#third-party-monitoring-services) ⭐ - configuration-driven backup software for servers and workstations
|
||||||
- [Radarr](https://radarr.video/) ⭐ - Movie collection manager for Usenet and BitTorrent users
|
- [Radarr](https://radarr.video/) ⭐ - Movie collection manager for Usenet and BitTorrent users
|
||||||
- [Sonarr](https://sonarr.tv/) ⭐ - PVR for Usenet and BitTorrent users
|
- [Sonarr](https://sonarr.tv/) ⭐ - PVR for Usenet and BitTorrent users
|
||||||
- [Gatus](https://gatus.io/) ⭐ - Automated service health dashboard
|
- [Gatus](https://gatus.io/) ⭐ - Automated service health dashboard
|
||||||
|
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
|
||||||
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
|
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
|
||||||
|
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||||
|
- [Scrt.link](https://scrt.link/) - Share a secret
|
||||||
- [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
|
||||||
|
|
||||||
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
|
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
|
||||||
@@ -47,11 +55,14 @@ messages until I finally finish implementing end-to-end encryption.
|
|||||||
- [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python)
|
- [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python)
|
||||||
- [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET)
|
- [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET)
|
||||||
- [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node)
|
- [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node)
|
||||||
|
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
|
||||||
|
- [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)
|
||||||
|
|
||||||
## CLIs + GUIs
|
## CLIs + GUIs
|
||||||
|
|
||||||
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
|
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
|
||||||
- [ntfy Desktop client](https://github.com/mininmobile/ntfy-desktop) - Cross-platform desktop application for ntfy
|
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
|
||||||
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
||||||
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
|
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
|
||||||
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
|
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
|
||||||
@@ -74,12 +85,14 @@ messages until I finally finish implementing end-to-end encryption.
|
|||||||
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
||||||
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
||||||
- [ntfy Discord bot](https://github.com/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
|
- [ntfy Discord bot](https://github.com/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
|
||||||
|
- [ntfy Discord bot](https://github.com/jr1221/ntfy_discord_bot) - An advanced modal-based bot for interacting with the ntfy.sh API (Dart)
|
||||||
- [Bettarr Notifications](https://github.com/NiNiyas/Bettarr-Notifications) - Better Notifications for Sonarr and Radarr (Python)
|
- [Bettarr Notifications](https://github.com/NiNiyas/Bettarr-Notifications) - Better Notifications for Sonarr and Radarr (Python)
|
||||||
- [Notify me the intruders](https://github.com/nothingbutlucas/notify_me_the_intruders) - Notify you if they are intruders or new connections on your network (Shell)
|
- [Notify me the intruders](https://github.com/nothingbutlucas/notify_me_the_intruders) - Notify you if they are intruders or new connections on your network (Shell)
|
||||||
- [Send GitHub Action to ntfy](https://github.com/NiNiyas/ntfy-action) - Send GitHub Action workflow notifications to ntfy (JS)
|
- [Send GitHub Action to ntfy](https://github.com/NiNiyas/ntfy-action) - Send GitHub Action workflow notifications to ntfy (JS)
|
||||||
- [ntfy alertmanager bridge](https://github.com/aTable/ntfy_alertmanager_bridge) - Basic alertmanager bridge to ntfy (JS)
|
- [aTable/ntfy alertmanager bridge](https://github.com/aTable/ntfy_alertmanager_bridge) - Basic alertmanager bridge to ntfy (JS)
|
||||||
- [ntfy-alertmanager](https://hub.xenrox.net/~xenrox/ntfy-alertmanager) - A bridge between ntfy and Alertmanager (Go)
|
- [~xenrox/ntfy-alertmanager](https://hub.xenrox.net/~xenrox/ntfy-alertmanager) - A bridge between ntfy and Alertmanager (Go)
|
||||||
- [alertmanager-ntfy](https://github.com/pinpox/alertmanager-ntfy) - Relay prometheus alertmanager alerts to ntfy (Go)
|
- [pinpox/alertmanager-ntfy](https://github.com/pinpox/alertmanager-ntfy) - Relay prometheus alertmanager alerts to ntfy (Go)
|
||||||
|
- [alexbakker/alertmanager-ntfy](https://github.com/alexbakker/alertmanager-ntfy) - Service that forwards Prometheus Alertmanager notifications to ntfy (Go)
|
||||||
- [restreamchat2ntfy](https://github.com/kurohuku7/restreamchat2ntfy) - Send restream.io chat to ntfy to check on the Meta Quest (JS)
|
- [restreamchat2ntfy](https://github.com/kurohuku7/restreamchat2ntfy) - Send restream.io chat to ntfy to check on the Meta Quest (JS)
|
||||||
- [k8s-ntfy-deployment-service](https://github.com/Christian42/k8s-ntfy-deployment-service) - Automatic Kubernetes (k8s) ntfy deployment
|
- [k8s-ntfy-deployment-service](https://github.com/Christian42/k8s-ntfy-deployment-service) - Automatic Kubernetes (k8s) ntfy deployment
|
||||||
- [huginn-global-entry-notif](https://github.com/kylezoa/huginn-global-entry-notif) - Checks CBP API for available appointments with Huginn (JSON)
|
- [huginn-global-entry-notif](https://github.com/kylezoa/huginn-global-entry-notif) - Checks CBP API for available appointments with Huginn (JSON)
|
||||||
@@ -89,38 +102,60 @@ messages until I finally finish implementing end-to-end encryption.
|
|||||||
- [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python)
|
- [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python)
|
||||||
- [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP)
|
- [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP)
|
||||||
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
||||||
- [ntfy-sdk](https://gitlab.com/p2kishimoto/ntfy-sdk) - ntfy client library to send notifications (Rust)
|
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
|
||||||
|
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
|
||||||
|
- [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell)
|
||||||
|
- [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java)
|
||||||
|
- [maubot-ntfy](https://gitlab.com/999eagle/maubot-ntfy) - Matrix bot to subscribe to ntfy topics and send messages to Matrix (Python)
|
||||||
|
- [ntfy-wrapper](https://github.com/vict0rsch/ntfy-wrapper) - Wrapper around ntfy (Python)
|
||||||
|
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
|
||||||
|
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
|
||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
- [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) - 11/2022
|
- [January 2023 Developer Update](https://community.nodebb.org/topic/16908/january-2023-developer-update) - nodebb.org - 1/2023
|
||||||
- [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - 11/2022
|
- [Comment envoyer des notifications push sur votre téléphone facilement et gratuitement?](https://korben.info/notifications-push-telephone.html) - 1/2023
|
||||||
- [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - 11/2022
|
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
|
||||||
- [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - 11/2022
|
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
|
||||||
- [Zero-cost push notifications to your phone or desktop via PUT/POST ](https://lobste.rs/s/41dq13/zero_cost_push_notifications_your_phone) - 10/2022
|
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
|
||||||
- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - 10/2022
|
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||||
- [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - 10/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
|
||||||
- [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - 9/2022
|
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||||
- [Ntfy, le service de notifications qu’il vous faut](https://www.cachem.fr/ntfy-le-service-de-notifications-quil-vous-faut/) - 9/2022
|
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||||
- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - 9/2022
|
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022
|
||||||
- [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - 8/2022
|
- [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022
|
||||||
- [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - 8/2022
|
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022
|
||||||
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - 8/2022
|
- [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - tabnews.com.br - 11/2022
|
||||||
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - 6/2022
|
- [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - volkerkrause.eu - 11/2022
|
||||||
- [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - 6/2022
|
- [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) ⭐ - tldr.tech - 11/2022
|
||||||
- [无需注册的通知服务ntfy](https://wbsu2003.4everland.app/2022/05/30/%E6%97%A0%E9%9C%80%E6%B3%A8%E5%86%8C%E7%9A%84%E9%80%9A%E7%9F%A5%E6%9C%8D%E5%8A%A1ntfy/) - 5/2022
|
- [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - news.ycombinator.com - 11/2022
|
||||||
- [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - 5/2022
|
- [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - lunarok-domotique.com - 11/2022
|
||||||
- [无需注册的通知服务ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - 5/2022
|
- [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - blog.parravidales.es - 11/2022
|
||||||
- [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - 4/2022
|
- [unRAID Notifications with ntfy.sh](https://lder.dev/posts/ntfy-Notifications-With-unRAID/) - lder.dev - 10/2022
|
||||||
- [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - 4/2022
|
- [Zero-cost push notifications to your phone or desktop via PUT/POST ](https://lobste.rs/s/41dq13/zero_cost_push_notifications_your_phone) - lobste.rs - 10/2022
|
||||||
- [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた (Gigazine)](https://gigazine.net/news/20220404-ntfy-push-notification/) - 4/2022
|
- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022
|
||||||
- [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - 3/2022
|
- [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - youtube.com - 10/2022
|
||||||
- [Reddit web app release post](https://www.reddit.com/r/selfhosted/comments/tc0p0u/say_hello_to_the_brand_new_ntfysh_web_app_push/) ⭐ - 3/2022
|
- [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - adminforge.de - 9/2022
|
||||||
- [Lemmy post (Jakob)](https://lemmy.eus/post/15541) - 1/2022
|
- [Ntfy, le service de notifications qu’il vous faut](https://www.cachem.fr/ntfy-le-service-de-notifications-quil-vous-faut/) - cachem.fr - 9/2022
|
||||||
- [Reddit UnifiedPush release post](https://www.reddit.com/r/selfhosted/comments/s5jylf/my_open_source_notification_android_app_and/) ⭐ - 1/2022
|
- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/2022
|
||||||
- [ntfy: send notifications from your computer to your phone](https://rs1.es/tutorials/2022/01/19/ntfy-send-notifications-phone.html) - 1/2022
|
- [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - thejeshgn.com - 8/2022
|
||||||
- [Short ntfy review (Jan-Lukas Else)](https://jlelse.blog/links/2021/12/ntfy-sh) - 12/2021
|
- [Fedora Magazine | 4 cool new projects to try in Copr](https://fedoramagazine.org/4-cool-new-projects-to-try-in-copr-for-august-2022/) - fedoramagazine.org - 8/2022
|
||||||
- [Free MacroDroid webhook alternative (FrameXX)](https://www.macrodroidforum.com/index.php?threads/ntfy-sh-free-macrodroid-webhook-alternative.1505/) - 12/2021
|
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - svrforum.com - 8/2022
|
||||||
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - 11/2021
|
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - sometimesir.com - 6/2022
|
||||||
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - 12/2021
|
- [ntfy is finally coming to iOS, and Matrix/UnifiedPush gateway support](https://www.reddit.com/r/selfhosted/comments/vdzvxi/ntfy_is_finally_coming_to_ios_with_full/) ⭐ - reddit.com - 6/2022
|
||||||
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - 11/2021
|
- [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - chowdera.com - 5/2022
|
||||||
|
- [无需注册的通知服务ntfy](https://blog.csdn.net/wbsu2004/article/details/125040247) - blog.csdn.net - 5/2022
|
||||||
|
- [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - jlelse.blog - 4/2022
|
||||||
|
- [Using ntfy and Tasker together](https://lachlanlife.net/posts/2022-04-tasker-ntfy/) - lachlanlife.net - 4/2022
|
||||||
|
- [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - reddit.com - 4/2022
|
||||||
|
- [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた](https://gigazine.net/news/20220404-ntfy-push-notification/) - gigazine.net - 4/2022
|
||||||
|
- [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - pocketmags.com - 3/2022
|
||||||
|
- [Reddit web app release post](https://www.reddit.com/r/selfhosted/comments/tc0p0u/say_hello_to_the_brand_new_ntfysh_web_app_push/) ⭐ - reddit.com- 3/2022
|
||||||
|
- [Lemmy post (Jakob)](https://lemmy.eus/post/15541) - lemmy.eus - 1/2022
|
||||||
|
- [Reddit UnifiedPush release post](https://www.reddit.com/r/selfhosted/comments/s5jylf/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 1/2022
|
||||||
|
- [ntfy: send notifications from your computer to your phone](https://rs1.es/tutorials/2022/01/19/ntfy-send-notifications-phone.html) - rs1.es - 1/2022
|
||||||
|
- [Short ntfy review (Jan-Lukas Else)](https://jlelse.blog/links/2021/12/ntfy-sh) - jlelse.blog - 12/2021
|
||||||
|
- [Free MacroDroid webhook alternative (FrameXX)](https://www.macrodroidforum.com/index.php?threads/ntfy-sh-free-macrodroid-webhook-alternative.1505/) - macrodroidforum.com - 12/2021
|
||||||
|
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021
|
||||||
|
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021
|
||||||
|
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021
|
||||||
|
|||||||
28
docs/known-issues.md
Normal file
28
docs/known-issues.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Known issues
|
||||||
|
This is an incomplete list of known issues with the ntfy server, 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
|
||||||
|
to have the prominent ones here to link to.
|
||||||
|
|
||||||
|
## iOS app not refreshing (see [#267](https://github.com/binwiederhier/ntfy/issues/267))
|
||||||
|
For some (many?) users, the iOS app is not refreshing the view when new notifications come in. Until you manually
|
||||||
|
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
|
||||||
|
|
||||||
|
This is caused by some weirdness between the Notification Service Extension (NSE), SwiftUI and Core Data. I am entirely
|
||||||
|
clueless on how to fix it, sadly, as it is ephemeral and now clear to me what is causing it.
|
||||||
|
|
||||||
|
Please send experienced iOS developers my way to help me figure this out.
|
||||||
|
|
||||||
|
## iOS app not receiving notifications (anymore)
|
||||||
|
If notifications do not show up at all anymore, there are a few causes for it (that I know of):
|
||||||
|
|
||||||
|
**Firebase+APNS are being weird and buggy**:
|
||||||
|
If this is the case, usually it helps to **remove the topic/subscription and re-add it**. That will force Firebase to
|
||||||
|
re-subscribe to the Firebase topic.
|
||||||
|
|
||||||
|
**Self-hosted only: No `upstream-base-url` set, or `base-url` mismatch**:
|
||||||
|
To make self-hosted servers work with the iOS
|
||||||
|
app, I had to do some horrible things (see [iOS instant notifications](config.md#ios-instant-notifications) for details).
|
||||||
|
Be sure that in your selfhosted server:
|
||||||
|
|
||||||
|
* 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
|
||||||
@@ -1316,7 +1316,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
=== "Command line (curl)"
|
=== "Command line (curl)"
|
||||||
```
|
```
|
||||||
curl \
|
curl \
|
||||||
-d "Somebody retweetet your tweet." \
|
-d "Somebody retweeted your tweet." \
|
||||||
-H "Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
-H "Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
||||||
ntfy.sh/myhome
|
ntfy.sh/myhome
|
||||||
```
|
```
|
||||||
@@ -1326,7 +1326,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
ntfy publish \
|
ntfy publish \
|
||||||
--actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
--actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
||||||
myhome \
|
myhome \
|
||||||
"Somebody retweetet your tweet."
|
"Somebody retweeted your tweet."
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "HTTP"
|
=== "HTTP"
|
||||||
@@ -1335,14 +1335,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
Host: ntfy.sh
|
Host: ntfy.sh
|
||||||
Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392
|
Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392
|
||||||
|
|
||||||
Somebody retweetet your tweet.
|
Somebody retweeted your tweet.
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "JavaScript"
|
=== "JavaScript"
|
||||||
``` javascript
|
``` javascript
|
||||||
fetch('https://ntfy.sh/myhome', {
|
fetch('https://ntfy.sh/myhome', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: 'Somebody retweetet your tweet.',
|
body: 'Somebody retweeted your tweet.',
|
||||||
headers: {
|
headers: {
|
||||||
'Actions': 'view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392'
|
'Actions': 'view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392'
|
||||||
}
|
}
|
||||||
@@ -1351,7 +1351,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
|
|
||||||
=== "Go"
|
=== "Go"
|
||||||
``` go
|
``` go
|
||||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Somebody retweetet your tweet."))
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Somebody retweeted your tweet."))
|
||||||
req.Header.Set("Actions", "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392")
|
req.Header.Set("Actions", "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392")
|
||||||
http.DefaultClient.Do(req)
|
http.DefaultClient.Do(req)
|
||||||
```
|
```
|
||||||
@@ -1360,14 +1360,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
``` powershell
|
``` powershell
|
||||||
$uri = "https://ntfy.sh/myhome"
|
$uri = "https://ntfy.sh/myhome"
|
||||||
$headers = @{ Actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" }
|
$headers = @{ Actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" }
|
||||||
$body = "Somebody retweetet your tweet."
|
$body = "Somebody retweeted your tweet."
|
||||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Python"
|
=== "Python"
|
||||||
``` python
|
``` python
|
||||||
requests.post("https://ntfy.sh/myhome",
|
requests.post("https://ntfy.sh/myhome",
|
||||||
data="Somebody retweetet your tweet.",
|
data="Somebody retweeted your tweet.",
|
||||||
headers={ "Actions": "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" })
|
headers={ "Actions": "view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" })
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1379,7 +1379,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
'header' =>
|
'header' =>
|
||||||
"Content-Type: text/plain\r\n" .
|
"Content-Type: text/plain\r\n" .
|
||||||
"Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392",
|
"Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392",
|
||||||
'content' => 'Somebody retweetet your tweet.'
|
'content' => 'Somebody retweeted your tweet.'
|
||||||
]
|
]
|
||||||
]));
|
]));
|
||||||
```
|
```
|
||||||
@@ -1391,7 +1391,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
curl ntfy.sh \
|
curl ntfy.sh \
|
||||||
-d '{
|
-d '{
|
||||||
"topic": "myhome",
|
"topic": "myhome",
|
||||||
"message": "Somebody retweetet your tweet.",
|
"message": "Somebody retweeted your tweet.",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"action": "view",
|
"action": "view",
|
||||||
@@ -1413,7 +1413,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
}
|
}
|
||||||
]' \
|
]' \
|
||||||
myhome \
|
myhome \
|
||||||
"Somebody retweetet your tweet."
|
"Somebody retweeted your tweet."
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "HTTP"
|
=== "HTTP"
|
||||||
@@ -1423,7 +1423,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
|
|
||||||
{
|
{
|
||||||
"topic": "myhome",
|
"topic": "myhome",
|
||||||
"message": "Somebody retweetet your tweet.",
|
"message": "Somebody retweeted your tweet.",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"action": "view",
|
"action": "view",
|
||||||
@@ -1440,7 +1440,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
topic: "myhome",
|
topic: "myhome",
|
||||||
message": "Somebody retweetet your tweet.",
|
message": "Somebody retweeted your tweet.",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: "view",
|
action: "view",
|
||||||
@@ -1459,7 +1459,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
|
|
||||||
body := `{
|
body := `{
|
||||||
"topic": "myhome",
|
"topic": "myhome",
|
||||||
"message": "Somebody retweetet your tweet.",
|
"message": "Somebody retweeted your tweet.",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"action": "view",
|
"action": "view",
|
||||||
@@ -1477,7 +1477,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
$uri = "https://ntfy.sh"
|
$uri = "https://ntfy.sh"
|
||||||
$body = @{
|
$body = @{
|
||||||
topic = "myhome"
|
topic = "myhome"
|
||||||
message = "Somebody retweetet your tweet."
|
message = "Somebody retweeted your tweet."
|
||||||
actions = @(
|
actions = @(
|
||||||
@{
|
@{
|
||||||
"action"="view"
|
"action"="view"
|
||||||
@@ -1494,7 +1494,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
requests.post("https://ntfy.sh/",
|
requests.post("https://ntfy.sh/",
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
"topic": "myhome",
|
"topic": "myhome",
|
||||||
"message": "Somebody retweetet your tweet.",
|
"message": "Somebody retweeted your tweet.",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"action": "view",
|
"action": "view",
|
||||||
@@ -1514,7 +1514,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
|||||||
'header' => "Content-Type: application/json",
|
'header' => "Content-Type: application/json",
|
||||||
'content' => json_encode([
|
'content' => json_encode([
|
||||||
"topic": "myhome",
|
"topic": "myhome",
|
||||||
"message": "Somebody retweetet your tweet.",
|
"message": "Somebody retweeted your tweet.",
|
||||||
"actions": [
|
"actions": [
|
||||||
[
|
[
|
||||||
"action": "view",
|
"action": "view",
|
||||||
|
|||||||
109
docs/releases.md
109
docs/releases.md
@@ -2,13 +2,116 @@
|
|||||||
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 v1.31.0
|
||||||
## ntfy Android app v1.14.0 (UNRELEASED)
|
Released February 14, 2023
|
||||||
|
|
||||||
|
This is a tiny release before the really big release, and also the last before the big v2.0.0. The most interesting
|
||||||
|
things in this release are the new preliminary health endpoint to allow monitoring in K8s (and others), and the removal
|
||||||
|
of `upx` binary packing (which was causing erroneous virus flagging). Aside from that, the `go-smtp` library did a
|
||||||
|
breaking-change upgrade, which required some work to get working again.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Preliminary `/v1/health` API endpoint for service monitoring (no ticket)
|
||||||
|
* Add basic health check to `Dockerfile` ([#555](https://github.com/binwiederhier/ntfy/pull/555), thanks to [@bt90](https://github.com/bt90))
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Fix `chown` issues with RHEL-like based systems ([#566](https://github.com/binwiederhier/ntfy/issues/566)/[#565](https://github.com/binwiederhier/ntfy/pull/565), thanks to [@danieldemus](https://github.com/danieldemus))
|
||||||
|
* Removed `upx` (binary packing) for all builds due to false virus warnings ([#576](https://github.com/binwiederhier/ntfy/issues/576), thanks to [@shawnhwei](https://github.com/shawnhwei) for reporting)
|
||||||
|
* Upgraded `go-smtp` library and tests to v0.16.0 ([#569](https://github.com/binwiederhier/ntfy/issues/569))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
* Add HTTP/2 and TLSv1.3 support to nginx docs ([#553](https://github.com/binwiederhier/ntfy/issues/553), thanks to [@bt90](https://github.com/bt90))
|
||||||
|
* Small wording change for `client.yml` ([#562](https://github.com/binwiederhier/ntfy/pull/562), thanks to [@fleopaulD](https://github.com/fleopaulD))
|
||||||
|
* Fix K8s install docs ([#582](https://github.com/binwiederhier/ntfy/pull/582), thanks to [@Remedan](https://github.com/Remedan))
|
||||||
|
* Updated Jellyseer docs ([#604](https://github.com/binwiederhier/ntfy/pull/604), thanks to [@Y0ngg4n](https://github.com/Y0ngg4n))
|
||||||
|
* Updated iOS developer docs ([#605](https://github.com/binwiederhier/ntfy/pull/605), thanks to [@SticksDev](https://github.com/SticksDev))
|
||||||
|
|
||||||
|
**Additional languages:**
|
||||||
|
|
||||||
|
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
|
||||||
|
|
||||||
|
## ntfy server v1.30.1
|
||||||
|
Released December 23, 2022 🎅
|
||||||
|
|
||||||
|
This is a special holiday edition version of ntfy, with all sorts of holiday fun and games, and hidden quests.
|
||||||
|
Nahh, just kidding. This release is an intermediate release mainly to eliminate warnings in the logs, so I can
|
||||||
|
roll out the TLSv1.3, HTTP/2 and Unix mode changes on ntfy.sh (see [#552](https://github.com/binwiederhier/ntfy/issues/552)).
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Web: Generate random topic name button ([#453](https://github.com/binwiederhier/ntfy/issues/453), thanks to [@yardenshoham](https://github.com/yardenshoham))
|
||||||
|
* Add [Gitpod config](https://github.com/binwiederhier/ntfy/blob/main/.gitpod.yml) ([#540](https://github.com/binwiederhier/ntfy/pull/540), thanks to [@yardenshoham](https://github.com/yardenshoham))
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Remove `--env-topic` option from `ntfy publish` as per [deprecation](deprecations.md) (no ticket)
|
||||||
|
* Prepared statements for message cache writes ([#542](https://github.com/binwiederhier/ntfy/pull/542), thanks to [@nicois](https://github.com/nicois))
|
||||||
|
* Do not warn about invalid IP address when behind proxy in unix socket mode (relates to [#552](https://github.com/binwiederhier/ntfy/issues/552))
|
||||||
|
* Upgrade nginx/ntfy config on ntfy.sh to work with TLSv1.3, HTTP/2 ([#552](https://github.com/binwiederhier/ntfy/issues/552), thanks to [@bt90](https://github.com/bt90))
|
||||||
|
|
||||||
|
## ntfy Android app v1.16.0
|
||||||
|
Released December 11, 2022
|
||||||
|
|
||||||
|
This is a feature and platform/dependency upgrade release. You can now have per-subscription notification settings
|
||||||
|
(including sounds, DND, etc.), and you can make notifications continue ringing until they are dismissed. There's also
|
||||||
|
support for thematic/adaptive launcher icon for Android 13.
|
||||||
|
|
||||||
|
There are a few more Android 13 specific things, as well as many bug fixes: No more crashes from large images, no more
|
||||||
|
opening the wrong subscription, and we also fixed the icon color issue.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Custom per-subscription notification settings incl. sounds, DND, etc. ([#6](https://github.com/binwiederhier/ntfy/issues/6), thanks to [@doits](https://github.com/doits))
|
||||||
|
* Insistent notifications that ring until dismissed ([#417](https://github.com/binwiederhier/ntfy/issues/417), thanks to [@danmed](https://github.com/danmed) for reporting)
|
||||||
|
* Add thematic/adaptive launcher icon ([#513](https://github.com/binwiederhier/ntfy/issues/513), thanks to [@daedric7](https://github.com/daedric7) for reporting)
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Upgrade Android dependencies and build toolchain to SDK 33 (no ticket)
|
||||||
|
* Simplify F-Droid build: Disable tasks for Google Services ([#516](https://github.com/binwiederhier/ntfy/issues/516), thanks to [@markosopcic](https://github.com/markosopcic))
|
||||||
|
* Android 13: Ask for permission to post notifications ([#508](https://github.com/binwiederhier/ntfy/issues/508))
|
||||||
|
* Android 13: Do not allow swiping away the foreground notification ([#521](https://github.com/binwiederhier/ntfy/issues/521), thanks to [@alexhorner](https://github.com/alexhorner) for reporting)
|
||||||
|
* Android 5 (SDK 21): Fix crash on unsubscribing ([#528](https://github.com/binwiederhier/ntfy/issues/528), thanks to Roger M.)
|
||||||
|
* Remove timestamp when copying message text ([#471](https://github.com/binwiederhier/ntfy/issues/471), thanks to [@wunter8](https://github.com/wunter8))
|
||||||
|
* Fix auto-delete if some icons do not exist anymore ([#506](https://github.com/binwiederhier/ntfy/issues/506))
|
||||||
|
* Fix notification icon color ([#480](https://github.com/binwiederhier/ntfy/issues/480), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d) for reporting)
|
||||||
|
* Fix topics do not re-subscribe to Firebase after restoring from backup ([#511](https://github.com/binwiederhier/ntfy/issues/511))
|
||||||
|
* Fix crashes from large images ([#474](https://github.com/binwiederhier/ntfy/issues/474), thanks to [@daedric7](https://github.com/daedric7) for reporting)
|
||||||
|
* Fix notification click opens wrong subscription ([#261](https://github.com/binwiederhier/ntfy/issues/261), thanks to [@SMAW](https://github.com/SMAW) for reporting)
|
||||||
|
* Fix Firebase-only "link expired" issue ([#529](https://github.com/binwiederhier/ntfy/issues/529))
|
||||||
|
* Remove "Install .apk" feature in Google Play variant due to policy change ([#531](https://github.com/binwiederhier/ntfy/issues/531))
|
||||||
|
* Add donate button (no ticket)
|
||||||
|
|
||||||
**Additional translations:**
|
**Additional translations:**
|
||||||
|
|
||||||
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
|
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
|
||||||
-->
|
* Portuguese (thanks to [@victormagalhaess](https://hosted.weblate.org/user/victormagalhaess/))
|
||||||
|
|
||||||
|
## ntfy server v1.29.1
|
||||||
|
Released November 17, 2022
|
||||||
|
|
||||||
|
This is mostly a bugfix release to address the high load on ntfy.sh. There are now two new options that allow
|
||||||
|
synchronous batch-writing of messages to the cache. This avoids database locking, and subsequent pileups of waiting
|
||||||
|
requests.
|
||||||
|
|
||||||
|
**Bug fixes:**
|
||||||
|
|
||||||
|
* High-load servers: Allow asynchronous batch-writing of messages to cache via `cache-batch-*` options ([#498](https://github.com/binwiederhier/ntfy/issues/498)/[#502](https://github.com/binwiederhier/ntfy/pull/502))
|
||||||
|
* Sender column in cache.db shows invalid IP ([#503](https://github.com/binwiederhier/ntfy/issues/503))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
* GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl))
|
||||||
|
* UnifiedPush ACL clarification ([#497](https://github.com/binwiederhier/ntfy/issues/497), thanks to [@bt90](https://github.com/bt90))
|
||||||
|
* Install instructions for Kustomize ([#463](https://github.com/binwiederhier/ntfy/pull/463), thanks to [@l-maciej](https://github.com/l-maciej))
|
||||||
|
|
||||||
|
**Other things:**
|
||||||
|
|
||||||
|
* Put ntfy.sh docs on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491))
|
||||||
|
* The ntfy.sh server hardware was upgraded to a bigger box. If you'd like to help out carrying the server cost, **[sponsorships and donations](https://github.com/sponsors/binwiederhier)** 💸 would be very much appreciated
|
||||||
|
|
||||||
## ntfy server v1.29.0
|
## ntfy server v1.29.0
|
||||||
Released November 12, 2022
|
Released November 12, 2022
|
||||||
|
|||||||
46
go.mod
46
go.mod
@@ -3,23 +3,23 @@ module heckel.io/ntfy
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.8.0 // indirect
|
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||||
cloud.google.com/go/storage v1.28.0 // indirect
|
cloud.google.com/go/storage v1.29.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/emersion/go-smtp v0.15.0
|
github.com/emersion/go-smtp v0.16.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1
|
github.com/gabriel-vasile/mimetype v1.4.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/urfave/cli/v2 v2.23.5
|
github.com/urfave/cli/v2 v2.24.3
|
||||||
golang.org/x/crypto v0.2.0
|
golang.org/x/crypto v0.6.0
|
||||||
golang.org/x/oauth2 v0.2.0 // indirect
|
golang.org/x/oauth2 v0.5.0 // indirect
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/term v0.2.0
|
golang.org/x/term v0.5.0
|
||||||
golang.org/x/time v0.2.0
|
golang.org/x/time v0.3.0
|
||||||
google.golang.org/api v0.103.0
|
google.golang.org/api v0.110.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,34 +28,34 @@ require github.com/pkg/errors v0.9.1 // indirect
|
|||||||
require firebase.google.com/go/v4 v4.10.0
|
require firebase.google.com/go/v4 v4.10.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.106.0 // indirect
|
cloud.google.com/go v0.109.0 // indirect
|
||||||
cloud.google.com/go/compute v1.12.1 // indirect
|
cloud.google.com/go/compute v1.18.0 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
cloud.google.com/go/iam v0.7.0 // indirect
|
cloud.google.com/go/iam v0.10.0 // indirect
|
||||||
cloud.google.com/go/longrunning v0.3.0 // indirect
|
cloud.google.com/go/longrunning v0.4.1 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/MicahParks/keyfunc v1.5.3 // indirect
|
github.com/MicahParks/keyfunc v1.9.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/v4 v4.4.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.4.3 // 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.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.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.2.0 // indirect
|
golang.org/x/net v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.2.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/appengine/v2 v2.0.2 // indirect
|
google.golang.org/appengine/v2 v2.0.2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e // indirect
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
|
||||||
google.golang.org/grpc v1.50.1 // indirect
|
google.golang.org/grpc v1.53.0 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
94
go.sum
94
go.sum
@@ -1,28 +1,27 @@
|
|||||||
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.106.0 h1:AWaMWuZb2oFeiV91OfNHZbmwUhMVuXEaLPm9sqDAOl8=
|
cloud.google.com/go v0.109.0 h1:38CZoKGlCnPZjGdyj0ZfpoGae0/wgNfy5F0byyxg0Gk=
|
||||||
cloud.google.com/go v0.106.0/go.mod h1:5NEGxGuIeMQiPaWLwLYZ7kfNWiP6w1+QJK+xqyIT+dw=
|
cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE=
|
||||||
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
|
cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
|
||||||
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
|
cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
|
||||||
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI=
|
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
|
||||||
cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
|
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
|
||||||
cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
|
cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
|
||||||
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
|
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
|
||||||
cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
|
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||||
cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
|
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||||
cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY=
|
cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||||
cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI=
|
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||||
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
|
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
|
||||||
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
|
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
|
||||||
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
|
||||||
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.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/MicahParks/keyfunc v1.5.3 h1:Y+mv+kX3HtL7/dCXXzK4bIDBHg91eunnGGkdndO0RWk=
|
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||||
github.com/MicahParks/keyfunc v1.5.3/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/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/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=
|
||||||
@@ -34,16 +33,17 @@ 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.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
|
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||||
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/emersion/go-smtp v0.16.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/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.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
|
||||||
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.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
@@ -75,17 +75,16 @@ github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1V
|
|||||||
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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||||
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||||
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
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=
|
||||||
@@ -96,21 +95,20 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
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/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
|
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||||
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
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=
|
||||||
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=
|
||||||
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.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
|
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
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=
|
||||||
@@ -124,11 +122,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||||||
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-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
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.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
|
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||||
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
|
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||||
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -140,19 +138,19 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
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-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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
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.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
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.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.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.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
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=
|
||||||
@@ -161,8 +159,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
|
|||||||
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-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
|
google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
|
||||||
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
|
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||||
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.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
@@ -172,15 +170,15 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho
|
|||||||
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-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-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M=
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
|
||||||
google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||||
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.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
|
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||||
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||||
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=
|
||||||
|
|||||||
@@ -87,8 +87,9 @@ nav:
|
|||||||
- "Examples": examples.md
|
- "Examples": examples.md
|
||||||
- "Integrations + projects": integrations.md
|
- "Integrations + projects": integrations.md
|
||||||
- "Release notes": releases.md
|
- "Release notes": releases.md
|
||||||
- "Deprecation notices": deprecations.md
|
|
||||||
- "Emojis 🥳 🎉": emojis.md
|
- "Emojis 🥳 🎉": emojis.md
|
||||||
|
- "Known issues": known-issues.md
|
||||||
|
- "Deprecation notices": deprecations.md
|
||||||
- "Development": develop.md
|
- "Development": develop.md
|
||||||
- "Privacy policy": privacy.md
|
- "Privacy policy": privacy.md
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ set -e
|
|||||||
if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
||||||
if [ -d /run/systemd/system ]; then
|
if [ -d /run/systemd/system ]; then
|
||||||
# Create ntfy user/group
|
# Create ntfy user/group
|
||||||
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home ntfy
|
groupadd -f ntfy
|
||||||
chown ntfy.ntfy /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home -g ntfy ntfy
|
||||||
|
chown ntfy:ntfy /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
||||||
chmod 700 /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
chmod 700 /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
||||||
|
|
||||||
# Hack to change permissions on cache file
|
# Hack to change permissions on cache file
|
||||||
@@ -16,7 +17,7 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
|||||||
if [ -f "$configfile" ]; then
|
if [ -f "$configfile" ]; then
|
||||||
cachefile="$(cat "$configfile" | perl -n -e'/^\s*cache-file: ["'"'"']?([^"'"'"']+)["'"'"']?/ && print $1')" # Oh my, see #47
|
cachefile="$(cat "$configfile" | perl -n -e'/^\s*cache-file: ["'"'"']?([^"'"'"']+)["'"'"']?/ && print $1')" # Oh my, see #47
|
||||||
if [ -n "$cachefile" ]; then
|
if [ -n "$cachefile" ]; then
|
||||||
chown ntfy.ntfy "$cachefile" || true
|
chown ntfy:ntfy "$cachefile" || true
|
||||||
chmod 600 "$cachefile" || true
|
chmod 600 "$cachefile" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ type Config struct {
|
|||||||
CacheFile string
|
CacheFile string
|
||||||
CacheDuration time.Duration
|
CacheDuration time.Duration
|
||||||
CacheStartupQueries string
|
CacheStartupQueries string
|
||||||
|
CacheBatchSize int
|
||||||
|
CacheBatchTimeout time.Duration
|
||||||
AuthFile string
|
AuthFile string
|
||||||
AuthDefaultRead bool
|
AuthDefaultRead bool
|
||||||
AuthDefaultWrite bool
|
AuthDefaultWrite bool
|
||||||
@@ -114,6 +116,8 @@ func NewConfig() *Config {
|
|||||||
FirebaseKeyFile: "",
|
FirebaseKeyFile: "",
|
||||||
CacheFile: "",
|
CacheFile: "",
|
||||||
CacheDuration: DefaultCacheDuration,
|
CacheDuration: DefaultCacheDuration,
|
||||||
|
CacheBatchSize: 0,
|
||||||
|
CacheBatchTimeout: 0,
|
||||||
AuthFile: "",
|
AuthFile: "",
|
||||||
AuthDefaultRead: true,
|
AuthDefaultRead: true,
|
||||||
AuthDefaultWrite: true,
|
AuthDefaultWrite: true,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const (
|
|||||||
published INT NOT NULL
|
published INT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
|
CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
|
||||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||||
COMMIT;
|
COMMIT;
|
||||||
`
|
`
|
||||||
@@ -92,7 +93,7 @@ const (
|
|||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
const (
|
const (
|
||||||
currentSchemaVersion = 8
|
currentSchemaVersion = 9
|
||||||
createSchemaVersionTableQuery = `
|
createSchemaVersionTableQuery = `
|
||||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
@@ -185,15 +186,21 @@ const (
|
|||||||
migrate7To8AlterMessagesTableQuery = `
|
migrate7To8AlterMessagesTableQuery = `
|
||||||
ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT('');
|
ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT('');
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// 8 -> 9
|
||||||
|
migrate8To9AlterMessagesTableQuery = `
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
type messageCache struct {
|
type messageCache struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
nop bool
|
queue *util.BatchingQueue[*message]
|
||||||
|
nop bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSqliteCache creates a SQLite file-backed cache
|
// newSqliteCache creates a SQLite file-backed cache
|
||||||
func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, error) {
|
func newSqliteCache(filename, startupQueries string, batchSize int, batchTimeout time.Duration, nop bool) (*messageCache, error) {
|
||||||
db, err := sql.Open("sqlite3", filename)
|
db, err := sql.Open("sqlite3", filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -201,21 +208,28 @@ func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, e
|
|||||||
if err := setupCacheDB(db, startupQueries); err != nil {
|
if err := setupCacheDB(db, startupQueries); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &messageCache{
|
var queue *util.BatchingQueue[*message]
|
||||||
db: db,
|
if batchSize > 0 || batchTimeout > 0 {
|
||||||
nop: nop,
|
queue = util.NewBatchingQueue[*message](batchSize, batchTimeout)
|
||||||
}, nil
|
}
|
||||||
|
cache := &messageCache{
|
||||||
|
db: db,
|
||||||
|
queue: queue,
|
||||||
|
nop: nop,
|
||||||
|
}
|
||||||
|
go cache.processMessageBatches()
|
||||||
|
return cache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMemCache creates an in-memory cache
|
// newMemCache creates an in-memory cache
|
||||||
func newMemCache() (*messageCache, error) {
|
func newMemCache() (*messageCache, error) {
|
||||||
return newSqliteCache(createMemoryFilename(), "", false)
|
return newSqliteCache(createMemoryFilename(), "", 0, 0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newNopCache creates an in-memory cache that discards all messages;
|
// newNopCache creates an in-memory cache that discards all messages;
|
||||||
// it is always empty and can be used if caching is entirely disabled
|
// it is always empty and can be used if caching is entirely disabled
|
||||||
func newNopCache() (*messageCache, error) {
|
func newNopCache() (*messageCache, error) {
|
||||||
return newSqliteCache(createMemoryFilename(), "", true)
|
return newSqliteCache(createMemoryFilename(), "", 0, 0, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createMemoryFilename creates a unique memory filename to use for the SQLite backend.
|
// createMemoryFilename creates a unique memory filename to use for the SQLite backend.
|
||||||
@@ -228,19 +242,36 @@ func createMemoryFilename() string {
|
|||||||
return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10))
|
return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddMessage stores a message to the message cache synchronously, or queues it to be stored at a later date asyncronously.
|
||||||
|
// The message is queued only if "batchSize" or "batchTimeout" are passed to the constructor.
|
||||||
func (c *messageCache) AddMessage(m *message) error {
|
func (c *messageCache) AddMessage(m *message) error {
|
||||||
|
if c.queue != nil {
|
||||||
|
c.queue.Enqueue(m)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return c.addMessages([]*message{m})
|
return c.addMessages([]*message{m})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until
|
||||||
|
// SQLite's busy_timeout is exceeded before erroring out.
|
||||||
func (c *messageCache) addMessages(ms []*message) error {
|
func (c *messageCache) addMessages(ms []*message) error {
|
||||||
if c.nop {
|
if c.nop {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if len(ms) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
tx, err := c.db.Begin()
|
tx, err := c.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
stmt, err := tx.Prepare(insertMessageQuery)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
for _, m := range ms {
|
for _, m := range ms {
|
||||||
if m.Event != messageEvent {
|
if m.Event != messageEvent {
|
||||||
return errUnexpectedMessageType
|
return errUnexpectedMessageType
|
||||||
@@ -264,8 +295,11 @@ func (c *messageCache) addMessages(ms []*message) error {
|
|||||||
}
|
}
|
||||||
actionsStr = string(actionsBytes)
|
actionsStr = string(actionsBytes)
|
||||||
}
|
}
|
||||||
_, err := tx.Exec(
|
var sender string
|
||||||
insertMessageQuery,
|
if m.Sender.IsValid() {
|
||||||
|
sender = m.Sender.String()
|
||||||
|
}
|
||||||
|
_, err := stmt.Exec(
|
||||||
m.ID,
|
m.ID,
|
||||||
m.Time,
|
m.Time,
|
||||||
m.Topic,
|
m.Topic,
|
||||||
@@ -281,7 +315,7 @@ func (c *messageCache) addMessages(ms []*message) error {
|
|||||||
attachmentSize,
|
attachmentSize,
|
||||||
attachmentExpires,
|
attachmentExpires,
|
||||||
attachmentURL,
|
attachmentURL,
|
||||||
m.Sender.String(),
|
sender,
|
||||||
m.Encoding,
|
m.Encoding,
|
||||||
published,
|
published,
|
||||||
)
|
)
|
||||||
@@ -289,7 +323,12 @@ func (c *messageCache) addMessages(ms []*message) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tx.Commit()
|
if err := tx.Commit(); err != nil {
|
||||||
|
log.Error("Cache: Writing %d message(s) failed (took %v)", len(ms), time.Since(start))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debug("Cache: Wrote %d message(s) in %v", len(ms), time.Since(start))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
|
func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
|
||||||
@@ -395,8 +434,12 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) Prune(olderThan time.Time) error {
|
func (c *messageCache) Prune(olderThan time.Time) error {
|
||||||
_, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix())
|
start := time.Now()
|
||||||
return err
|
if _, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix()); err != nil {
|
||||||
|
log.Warn("Cache: Pruning failed (after %v): %s", time.Since(start), err.Error())
|
||||||
|
}
|
||||||
|
log.Debug("Cache: Pruning successful (took %v)", time.Since(start))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
|
func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
|
||||||
@@ -417,6 +460,17 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *messageCache) processMessageBatches() {
|
||||||
|
if c.queue == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for messages := range c.queue.Dequeue() {
|
||||||
|
if err := c.addMessages(messages); err != nil {
|
||||||
|
log.Error("Cache: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func readMessages(rows *sql.Rows) ([]*message, error) {
|
func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
messages := make([]*message, 0)
|
messages := make([]*message, 0)
|
||||||
@@ -458,9 +512,8 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
|||||||
}
|
}
|
||||||
senderIP, err := netip.ParseAddr(sender)
|
senderIP, err := netip.ParseAddr(sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
senderIP = netip.IPv4Unspecified() // if no IP stored in database, 0.0.0.0
|
senderIP = netip.Addr{} // if no IP stored in database, return invalid address
|
||||||
}
|
}
|
||||||
|
|
||||||
var att *attachment
|
var att *attachment
|
||||||
if attachmentName != "" && attachmentURL != "" {
|
if attachmentName != "" && attachmentURL != "" {
|
||||||
att = &attachment{
|
att = &attachment{
|
||||||
@@ -542,6 +595,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
|
|||||||
return migrateFrom6(db)
|
return migrateFrom6(db)
|
||||||
} else if schemaVersion == 7 {
|
} else if schemaVersion == 7 {
|
||||||
return migrateFrom7(db)
|
return migrateFrom7(db)
|
||||||
|
} else if schemaVersion == 8 {
|
||||||
|
return migrateFrom8(db)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
||||||
}
|
}
|
||||||
@@ -647,5 +702,16 @@ func migrateFrom7(db *sql.DB) error {
|
|||||||
if _, err := db.Exec(updateSchemaVersion, 8); err != nil {
|
if _, err := db.Exec(updateSchemaVersion, 8); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return migrateFrom8(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateFrom8(db *sql.DB) error {
|
||||||
|
log.Info("Migrating cache database schema: from 8 to 9")
|
||||||
|
if _, err := db.Exec(migrate8To9AlterMessagesTableQuery); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(updateSchemaVersion, 9); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil // Update this when a new version is added
|
return nil // Update this when a new version is added
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ func TestSqliteCache_StartupQueries_WAL(t *testing.T) {
|
|||||||
startupQueries := `pragma journal_mode = WAL;
|
startupQueries := `pragma journal_mode = WAL;
|
||||||
pragma synchronous = normal;
|
pragma synchronous = normal;
|
||||||
pragma temp_store = memory;`
|
pragma temp_store = memory;`
|
||||||
db, err := newSqliteCache(filename, startupQueries, false)
|
db, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
||||||
require.FileExists(t, filename)
|
require.FileExists(t, filename)
|
||||||
@@ -461,7 +461,7 @@ pragma temp_store = memory;`
|
|||||||
func TestSqliteCache_StartupQueries_None(t *testing.T) {
|
func TestSqliteCache_StartupQueries_None(t *testing.T) {
|
||||||
filename := newSqliteTestCacheFile(t)
|
filename := newSqliteTestCacheFile(t)
|
||||||
startupQueries := ""
|
startupQueries := ""
|
||||||
db, err := newSqliteCache(filename, startupQueries, false)
|
db, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
||||||
require.FileExists(t, filename)
|
require.FileExists(t, filename)
|
||||||
@@ -472,10 +472,33 @@ func TestSqliteCache_StartupQueries_None(t *testing.T) {
|
|||||||
func TestSqliteCache_StartupQueries_Fail(t *testing.T) {
|
func TestSqliteCache_StartupQueries_Fail(t *testing.T) {
|
||||||
filename := newSqliteTestCacheFile(t)
|
filename := newSqliteTestCacheFile(t)
|
||||||
startupQueries := `xx error`
|
startupQueries := `xx error`
|
||||||
_, err := newSqliteCache(filename, startupQueries, false)
|
_, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSqliteCache_Sender(t *testing.T) {
|
||||||
|
testSender(t, newSqliteTestCache(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMemCache_Sender(t *testing.T) {
|
||||||
|
testSender(t, newMemTestCache(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSender(t *testing.T, c *messageCache) {
|
||||||
|
m1 := newDefaultMessage("mytopic", "mymessage")
|
||||||
|
m1.Sender = netip.MustParseAddr("1.2.3.4")
|
||||||
|
require.Nil(t, c.AddMessage(m1))
|
||||||
|
|
||||||
|
m2 := newDefaultMessage("mytopic", "mymessage without sender")
|
||||||
|
require.Nil(t, c.AddMessage(m2))
|
||||||
|
|
||||||
|
messages, err := c.Messages("mytopic", sinceAllMessages, false)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, 2, len(messages))
|
||||||
|
require.Equal(t, messages[0].Sender, netip.MustParseAddr("1.2.3.4"))
|
||||||
|
require.Equal(t, messages[1].Sender, netip.Addr{})
|
||||||
|
}
|
||||||
|
|
||||||
func checkSchemaVersion(t *testing.T, db *sql.DB) {
|
func checkSchemaVersion(t *testing.T, db *sql.DB) {
|
||||||
rows, err := db.Query(`SELECT version FROM schemaVersion`)
|
rows, err := db.Query(`SELECT version FROM schemaVersion`)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@@ -501,7 +524,7 @@ func TestMemCache_NopCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSqliteTestCache(t *testing.T) *messageCache {
|
func newSqliteTestCache(t *testing.T) *messageCache {
|
||||||
c, err := newSqliteCache(newSqliteTestCacheFile(t), "", false)
|
c, err := newSqliteCache(newSqliteTestCacheFile(t), "", 0, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -513,7 +536,7 @@ func newSqliteTestCacheFile(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache {
|
func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache {
|
||||||
c, err := newSqliteCache(filename, startupQueries, false)
|
c, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ var (
|
|||||||
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
|
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
|
||||||
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
||||||
|
|
||||||
|
healthPath = "/v1/health"
|
||||||
webConfigPath = "/config.js"
|
webConfigPath = "/config.js"
|
||||||
userStatsPath = "/user/stats"
|
userStatsPath = "/user/stats"
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
@@ -159,7 +160,7 @@ func createMessageCache(conf *Config) (*messageCache, error) {
|
|||||||
if conf.CacheDuration == 0 {
|
if conf.CacheDuration == 0 {
|
||||||
return newNopCache()
|
return newNopCache()
|
||||||
} else if conf.CacheFile != "" {
|
} else if conf.CacheFile != "" {
|
||||||
return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, false)
|
return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, conf.CacheBatchSize, conf.CacheBatchTimeout, false)
|
||||||
}
|
}
|
||||||
return newMemCache()
|
return newMemCache()
|
||||||
}
|
}
|
||||||
@@ -296,6 +297,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||||
|
} else if r.Method == http.MethodGet && r.URL.Path == healthPath {
|
||||||
|
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 == userStatsPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
||||||
@@ -366,6 +369,18 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
|
response := &apiHealthResponse{
|
||||||
|
Healthy: true,
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
appRoot := "/"
|
appRoot := "/"
|
||||||
if !s.config.WebRootIsApp {
|
if !s.config.WebRootIsApp {
|
||||||
@@ -491,6 +506,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
|
|||||||
log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
|
log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
|
||||||
}
|
}
|
||||||
if cache {
|
if cache {
|
||||||
|
log.Debug("%s Adding message to cache", logMessagePrefix(v, m))
|
||||||
if err := s.messageCache.AddMessage(m); err != nil {
|
if err := s.messageCache.AddMessage(m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1444,7 +1460,9 @@ func (s *Server) visitor(r *http.Request) *visitor {
|
|||||||
ip, err = netip.ParseAddr(remoteAddr)
|
ip, err = netip.ParseAddr(remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ip = netip.IPv4Unspecified()
|
ip = netip.IPv4Unspecified()
|
||||||
log.Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created %s", remoteAddr, err)
|
if remoteAddr != "@" || !s.config.BehindProxy { // RemoteAddr is @ when unix socket is used
|
||||||
|
log.Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created %s", remoteAddr, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.config.BehindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
|
if s.config.BehindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
|
||||||
|
|||||||
@@ -53,6 +53,12 @@
|
|||||||
# pragma journal_mode = WAL;
|
# pragma journal_mode = WAL;
|
||||||
# pragma synchronous = normal;
|
# pragma synchronous = normal;
|
||||||
# pragma temp_store = memory;
|
# pragma temp_store = memory;
|
||||||
|
# pragma busy_timeout = 15000;
|
||||||
|
# vacuum;
|
||||||
|
#
|
||||||
|
# The "cache-batch-size" and "cache-batch-timeout" parameter allow enabling async batch writing
|
||||||
|
# of messages. If set, messages will be queued and written to the database in batches of the given
|
||||||
|
# size, or after the given timeout. This is only required for high volume servers.
|
||||||
#
|
#
|
||||||
# Debian/RPM package users:
|
# Debian/RPM package users:
|
||||||
# Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package
|
# Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package
|
||||||
@@ -65,6 +71,8 @@
|
|||||||
# cache-file: <filename>
|
# cache-file: <filename>
|
||||||
# cache-duration: "12h"
|
# cache-duration: "12h"
|
||||||
# cache-startup-queries:
|
# cache-startup-queries:
|
||||||
|
# cache-batch-size: 0
|
||||||
|
# cache-batch-timeout: "0ms"
|
||||||
|
|
||||||
# If set, access to the ntfy server and API can be controlled on a granular level using
|
# If set, access to the ntfy server and API can be controlled on a granular level using
|
||||||
# the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs.
|
# the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs.
|
||||||
@@ -173,8 +181,9 @@
|
|||||||
# Rate limiting: Allowed GET/PUT/POST requests per second, per visitor:
|
# Rate limiting: Allowed GET/PUT/POST requests per second, per visitor:
|
||||||
# - visitor-request-limit-burst is the initial bucket of requests each visitor has
|
# - visitor-request-limit-burst is the initial bucket of requests each visitor has
|
||||||
# - visitor-request-limit-replenish is the rate at which the bucket is refilled
|
# - visitor-request-limit-replenish is the rate at which the bucket is refilled
|
||||||
# - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames and IPs to be
|
# - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames, IPs or CIDRs to be
|
||||||
# exempt from request rate limiting; hostnames are resolved at the time the server is started
|
# exempt from request rate limiting. Hostnames are resolved at the time the server is started.
|
||||||
|
# Example: "1.2.3.4,ntfy.example.com,8.7.6.0/24"
|
||||||
#
|
#
|
||||||
# visitor-request-limit-burst: 60
|
# visitor-request-limit-burst: 60
|
||||||
# visitor-request-limit-replenish: "5s"
|
# visitor-request-limit-replenish: "5s"
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ type smtpBackend struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ smtp.Backend = (*smtpBackend)(nil)
|
||||||
|
var _ smtp.Session = (*smtpSession)(nil)
|
||||||
|
|
||||||
func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend {
|
func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend {
|
||||||
return &smtpBackend{
|
return &smtpBackend{
|
||||||
config: conf,
|
config: conf,
|
||||||
@@ -41,14 +44,9 @@ func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
func (b *smtpBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
||||||
log.Debug("%s Incoming mail, login with user %s", logSMTPPrefix(state), username)
|
log.Debug("%s Incoming mail", logSMTPPrefix(conn))
|
||||||
return &smtpSession{backend: b, state: state}, nil
|
return &smtpSession{backend: b, conn: conn}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
|
||||||
log.Debug("%s Incoming mail, anonymous login", logSMTPPrefix(state))
|
|
||||||
return &smtpSession{backend: b, state: state}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
|
func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
|
||||||
@@ -60,23 +58,23 @@ func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
|
|||||||
// smtpSession is returned after EHLO.
|
// smtpSession is returned after EHLO.
|
||||||
type smtpSession struct {
|
type smtpSession struct {
|
||||||
backend *smtpBackend
|
backend *smtpBackend
|
||||||
state *smtp.ConnectionState
|
conn *smtp.Conn
|
||||||
topic string
|
topic string
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
func (s *smtpSession) AuthPlain(username, _ string) error {
|
||||||
log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.state), username)
|
log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.conn), username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
||||||
log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.state), from, opts)
|
log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.conn), from, opts)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Rcpt(to string) error {
|
func (s *smtpSession) Rcpt(to string) error {
|
||||||
log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.state), to)
|
log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.conn), to)
|
||||||
return s.withFailCount(func() error {
|
return s.withFailCount(func() error {
|
||||||
conf := s.backend.config
|
conf := s.backend.config
|
||||||
addressList, err := mail.ParseAddressList(to)
|
addressList, err := mail.ParseAddressList(to)
|
||||||
@@ -114,9 +112,9 @@ func (s *smtpSession) Data(r io.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if log.IsTrace() {
|
if log.IsTrace() {
|
||||||
log.Trace("%s DATA: %s", logSMTPPrefix(s.state), string(b))
|
log.Trace("%s DATA: %s", logSMTPPrefix(s.conn), string(b))
|
||||||
} else if log.IsDebug() {
|
} else if log.IsDebug() {
|
||||||
log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.state), len(b))
|
log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.conn), len(b))
|
||||||
}
|
}
|
||||||
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -156,9 +154,9 @@ func (s *smtpSession) Data(r io.Reader) error {
|
|||||||
|
|
||||||
func (s *smtpSession) publishMessage(m *message) error {
|
func (s *smtpSession) publishMessage(m *message) error {
|
||||||
// Extract remote address (for rate limiting)
|
// Extract remote address (for rate limiting)
|
||||||
remoteAddr, _, err := net.SplitHostPort(s.state.RemoteAddr.String())
|
remoteAddr, _, err := net.SplitHostPort(s.conn.Conn().RemoteAddr().String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
remoteAddr = s.state.RemoteAddr.String()
|
remoteAddr = s.conn.Conn().RemoteAddr().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call HTTP handler with fake HTTP request
|
// Call HTTP handler with fake HTTP request
|
||||||
@@ -198,7 +196,7 @@ func (s *smtpSession) withFailCount(fn func() error) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// Almost all of these errors are parse errors, and user input errors.
|
// Almost all of these errors are parse errors, and user input errors.
|
||||||
// We do not want to spam the log with WARN messages.
|
// We do not want to spam the log with WARN messages.
|
||||||
log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.state), err.Error())
|
log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.conn), err.Error())
|
||||||
s.backend.failure++
|
s.backend.failure++
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSmtpBackend_Multipart(t *testing.T) {
|
func TestSmtpBackend_Multipart(t *testing.T) {
|
||||||
email := `MIME-Version: 1.0
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
MIME-Version: 1.0
|
||||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||||
Subject: and one more
|
Subject: and one more
|
||||||
@@ -28,20 +35,25 @@ Content-Type: text/html; charset="UTF-8"
|
|||||||
|
|
||||||
<div dir="ltr">what's up<br clear="all"><div><br></div></div>
|
<div dir="ltr">what's up<br clear="all"><div><br></div></div>
|
||||||
|
|
||||||
--000000000000f3320b05d42915c9--`
|
--000000000000f3320b05d42915c9--
|
||||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
.
|
||||||
|
`
|
||||||
|
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
require.Equal(t, "and one more", r.Header.Get("Title"))
|
require.Equal(t, "and one more", r.Header.Get("Title"))
|
||||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||||
})
|
})
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
defer s.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
defer c.Close()
|
||||||
require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_MultipartNoBody(t *testing.T) {
|
func TestSmtpBackend_MultipartNoBody(t *testing.T) {
|
||||||
email := `MIME-Version: 1.0
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: ntfy-emailtest@ntfy.sh
|
||||||
|
DATA
|
||||||
|
MIME-Version: 1.0
|
||||||
Date: Tue, 28 Dec 2021 01:33:34 +0100
|
Date: Tue, 28 Dec 2021 01:33:34 +0100
|
||||||
Message-ID: <CAAvm7ABCDsi9vsuu0WTRXzZQBC8dXrDOLT8iCWdqrsmg@mail.gmail.com>
|
Message-ID: <CAAvm7ABCDsi9vsuu0WTRXzZQBC8dXrDOLT8iCWdqrsmg@mail.gmail.com>
|
||||||
Subject: This email has a subject but no body
|
Subject: This email has a subject but no body
|
||||||
@@ -59,20 +71,25 @@ Content-Type: text/html; charset="UTF-8"
|
|||||||
|
|
||||||
<div dir="ltr"><br></div>
|
<div dir="ltr"><br></div>
|
||||||
|
|
||||||
--000000000000bcf4a405d429f8d4--`
|
--000000000000bcf4a405d429f8d4--
|
||||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
.
|
||||||
|
`
|
||||||
|
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "/emailtest", r.URL.Path)
|
require.Equal(t, "/emailtest", r.URL.Path)
|
||||||
require.Equal(t, "", r.Header.Get("Title")) // We flipped message and body
|
require.Equal(t, "", r.Header.Get("Title")) // We flipped message and body
|
||||||
require.Equal(t, "This email has a subject but no body", readAll(t, r.Body))
|
require.Equal(t, "This email has a subject but no body", readAll(t, r.Body))
|
||||||
})
|
})
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
defer s.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
defer c.Close()
|
||||||
require.Nil(t, session.Rcpt("ntfy-emailtest@ntfy.sh"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Plaintext(t *testing.T) {
|
func TestSmtpBackend_Plaintext(t *testing.T) {
|
||||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||||
Subject: and one more
|
Subject: and one more
|
||||||
From: Phil <phil@example.com>
|
From: Phil <phil@example.com>
|
||||||
@@ -80,56 +97,68 @@ To: mytopic@ntfy.sh
|
|||||||
Content-Type: text/plain; charset="UTF-8"
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
what's up
|
what's up
|
||||||
|
.
|
||||||
`
|
`
|
||||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
require.Equal(t, "and one more", r.Header.Get("Title"))
|
require.Equal(t, "and one more", r.Header.Get("Title"))
|
||||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||||
})
|
})
|
||||||
conf.SMTPServerAddrPrefix = ""
|
conf.SMTPServerAddrPrefix = ""
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
defer s.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
defer c.Close()
|
||||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
|
func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
|
||||||
email := `Subject: Very short mail
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Subject: Very short mail
|
||||||
|
|
||||||
what's up
|
what's up
|
||||||
|
.
|
||||||
`
|
`
|
||||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
require.Equal(t, "Very short mail", r.Header.Get("Title"))
|
require.Equal(t, "Very short mail", r.Header.Get("Title"))
|
||||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||||
})
|
})
|
||||||
conf.SMTPServerAddrPrefix = ""
|
conf.SMTPServerAddrPrefix = ""
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
defer s.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
defer c.Close()
|
||||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
|
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
|
||||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
|
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
|
||||||
From: Phil <phil@example.com>
|
From: Phil <phil@example.com>
|
||||||
To: ntfy-mytopic@ntfy.sh
|
To: ntfy-mytopic@ntfy.sh
|
||||||
Content-Type: text/plain; charset="UTF-8"
|
Content-Type: text/plain; charset="UTF-8"
|
||||||
|
|
||||||
what's up
|
what's up
|
||||||
|
.
|
||||||
`
|
`
|
||||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "Three santas 🎅🎅🎅", r.Header.Get("Title"))
|
require.Equal(t, "Three santas 🎅🎅🎅", r.Header.Get("Title"))
|
||||||
})
|
})
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
defer s.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
defer c.Close()
|
||||||
require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Plaintext_TooLongTruncate(t *testing.T) {
|
func TestSmtpBackend_Plaintext_TooLongTruncate(t *testing.T) {
|
||||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||||
Subject: and one more
|
Subject: and one more
|
||||||
From: Phil <phil@example.com>
|
From: Phil <phil@example.com>
|
||||||
@@ -148,60 +177,61 @@ so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
that should do it
|
that should do it
|
||||||
|
.
|
||||||
`
|
`
|
||||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
expected := `you know this is a string.
|
expected := `you know this is a string.
|
||||||
it's a long string.
|
it's a long string.
|
||||||
it's supposed to be longer than the max message length
|
it's supposed to be longer than the max message length
|
||||||
@@ -214,68 +244,71 @@ so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
......................................................................
|
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||||
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
BBBBBBBBBBBBBBBBBBBBBBBBB`
|
BBBBBBBBBBBBBBBBBBBBBBBBB`
|
||||||
require.Equal(t, 4096, len(expected)) // Sanity check
|
require.Equal(t, 4096, len(expected)) // Sanity check
|
||||||
require.Equal(t, expected, readAll(t, r.Body))
|
require.Equal(t, expected, readAll(t, r.Body))
|
||||||
})
|
})
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
conf.SMTPServerAddrPrefix = ""
|
conf.SMTPServerAddrPrefix = ""
|
||||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
|
||||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
|
||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Unsupported(t *testing.T) {
|
func TestSmtpBackend_Unsupported(t *testing.T) {
|
||||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||||
Subject: and one more
|
Subject: and one more
|
||||||
From: Phil <phil@example.com>
|
From: Phil <phil@example.com>
|
||||||
@@ -283,34 +316,89 @@ To: mytopic@ntfy.sh
|
|||||||
Content-Type: text/SOMETHINGELSE
|
Content-Type: text/SOMETHINGELSE
|
||||||
|
|
||||||
what's up
|
what's up
|
||||||
|
.
|
||||||
`
|
`
|
||||||
conf, backend := newTestBackend(t, func(http.ResponseWriter, *http.Request) {
|
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Nothing.
|
t.Fatal("This should not be called")
|
||||||
})
|
})
|
||||||
conf.SMTPServerAddrPrefix = ""
|
defer s.Close()
|
||||||
session, _ := backend.Login(fakeConnState(t, "1.2.3.4"), "user", "pass")
|
defer c.Close()
|
||||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: unsupported content type")
|
||||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
|
||||||
require.Equal(t, errUnsupportedContentType, session.Data(strings.NewReader(email)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*Config, *smtpBackend) {
|
func TestSmtpBackend_InvalidAddress(t *testing.T) {
|
||||||
conf := newTestConfig(t)
|
email := `EHLO example.com
|
||||||
|
MAIL FROM: phil@example.com
|
||||||
|
RCPT TO: unsupported@ntfy.sh
|
||||||
|
DATA
|
||||||
|
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
|
Subject: and one more
|
||||||
|
From: Phil <phil@example.com>
|
||||||
|
To: mytopic@ntfy.sh
|
||||||
|
Content-Type: text/plain
|
||||||
|
|
||||||
|
what's up
|
||||||
|
.
|
||||||
|
`
|
||||||
|
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Fatal("This should not be called")
|
||||||
|
})
|
||||||
|
defer s.Close()
|
||||||
|
defer c.Close()
|
||||||
|
writeAndReadUntilLine(t, email, c, scanner, "451 4.0.0 invalid address")
|
||||||
|
}
|
||||||
|
|
||||||
|
type smtpHandlerFunc func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) {
|
||||||
|
conf = newTestConfig(t)
|
||||||
conf.SMTPServerListen = ":25"
|
conf.SMTPServerListen = ":25"
|
||||||
conf.SMTPServerDomain = "ntfy.sh"
|
conf.SMTPServerDomain = "ntfy.sh"
|
||||||
conf.SMTPServerAddrPrefix = "ntfy-"
|
conf.SMTPServerAddrPrefix = "ntfy-"
|
||||||
backend := newMailBackend(conf, handler)
|
backend := newMailBackend(conf, handler)
|
||||||
return conf, backend
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
}
|
|
||||||
|
|
||||||
func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState {
|
|
||||||
ip, err := net.ResolveIPAddr("ip", remoteAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
return &smtp.ConnectionState{
|
s = smtp.NewServer(backend)
|
||||||
Hostname: "myhostname",
|
s.Domain = conf.SMTPServerDomain
|
||||||
LocalAddr: ip,
|
s.AllowInsecureAuth = true
|
||||||
RemoteAddr: ip,
|
go func() {
|
||||||
|
require.Nil(t, s.Serve(l))
|
||||||
|
}()
|
||||||
|
c, err = net.Dial("tcp", l.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
scanner = bufio.NewScanner(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAndReadUntilLine(t *testing.T, email string, conn net.Conn, scanner *bufio.Scanner, expectedLine string) {
|
||||||
|
_, err := io.WriteString(conn, email)
|
||||||
|
require.Nil(t, err)
|
||||||
|
readUntilLine(t, conn, scanner, expectedLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUntilLine(t *testing.T, conn net.Conn, scanner *bufio.Scanner, expectedLine string) {
|
||||||
|
cancelChan := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-cancelChan:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
conn.Close()
|
||||||
|
t.Error("Failed waiting for expected output")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var output string
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
if strings.TrimSpace(text) == expectedLine {
|
||||||
|
cancelChan <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output += text + "\n"
|
||||||
|
//fmt.Println(text)
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected line '%s' not found in output:\n%s", expectedLine, output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,3 +213,7 @@ func (q *queryFilter) Pass(msg *message) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiHealthResponse struct {
|
||||||
|
Healthy bool `json:"healthy"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ func logHTTPPrefix(v *visitor, r *http.Request) string {
|
|||||||
return fmt.Sprintf("%s HTTP %s %s", v.ip, r.Method, requestURI)
|
return fmt.Sprintf("%s HTTP %s %s", v.ip, r.Method, requestURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logSMTPPrefix(state *smtp.ConnectionState) string {
|
func logSMTPPrefix(conn *smtp.Conn) string {
|
||||||
return fmt.Sprintf("%s/%s SMTP", state.Hostname, state.RemoteAddr.String())
|
return fmt.Sprintf("%s/%s SMTP", conn.Hostname(), conn.Conn().RemoteAddr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderHTTPRequest(r *http.Request) string {
|
func renderHTTPRequest(r *http.Request) string {
|
||||||
|
|||||||
86
util/batching_queue.go
Normal file
86
util/batching_queue.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatchingQueue is a queue that creates batches of the enqueued elements based on a
|
||||||
|
// max batch size and a batch timeout.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// q := NewBatchingQueue[int](2, 500 * time.Millisecond)
|
||||||
|
// go func() {
|
||||||
|
// for batch := range q.Dequeue() {
|
||||||
|
// fmt.Println(batch)
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
// q.Enqueue(1)
|
||||||
|
// q.Enqueue(2)
|
||||||
|
// q.Enqueue(3)
|
||||||
|
// time.Sleep(time.Second)
|
||||||
|
//
|
||||||
|
// This example will emit batch [1, 2] immediately (because the batch size is 2), and
|
||||||
|
// a batch [3] after 500ms.
|
||||||
|
type BatchingQueue[T any] struct {
|
||||||
|
batchSize int
|
||||||
|
timeout time.Duration
|
||||||
|
in []T
|
||||||
|
out chan []T
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBatchingQueue creates a new BatchingQueue
|
||||||
|
func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueue[T] {
|
||||||
|
q := &BatchingQueue[T]{
|
||||||
|
batchSize: batchSize,
|
||||||
|
timeout: timeout,
|
||||||
|
in: make([]T, 0),
|
||||||
|
out: make(chan []T),
|
||||||
|
}
|
||||||
|
go q.timeoutTicker()
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue enqueues an element to the queue. If the configured batch size is reached,
|
||||||
|
// the batch will be emitted immediately.
|
||||||
|
func (q *BatchingQueue[T]) Enqueue(element T) {
|
||||||
|
q.mu.Lock()
|
||||||
|
q.in = append(q.in, element)
|
||||||
|
var elements []T
|
||||||
|
if len(q.in) == q.batchSize {
|
||||||
|
elements = q.dequeueAll()
|
||||||
|
}
|
||||||
|
q.mu.Unlock()
|
||||||
|
if len(elements) > 0 {
|
||||||
|
q.out <- elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue returns a channel emitting batches of elements
|
||||||
|
func (q *BatchingQueue[T]) Dequeue() <-chan []T {
|
||||||
|
return q.out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *BatchingQueue[T]) dequeueAll() []T {
|
||||||
|
elements := make([]T, len(q.in))
|
||||||
|
copy(elements, q.in)
|
||||||
|
q.in = q.in[:0]
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *BatchingQueue[T]) timeoutTicker() {
|
||||||
|
if q.timeout == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(q.timeout)
|
||||||
|
for range ticker.C {
|
||||||
|
q.mu.Lock()
|
||||||
|
elements := q.dequeueAll()
|
||||||
|
q.mu.Unlock()
|
||||||
|
if len(elements) > 0 {
|
||||||
|
q.out <- elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
util/batching_queue_test.go
Normal file
58
util/batching_queue_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package util_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBatchingQueue_InfTimeout(t *testing.T) {
|
||||||
|
q := util.NewBatchingQueue[int](25, 1*time.Hour)
|
||||||
|
batches, total := make([][]int, 0), 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
go func() {
|
||||||
|
for batch := range q.Dequeue() {
|
||||||
|
mu.Lock()
|
||||||
|
batches = append(batches, batch)
|
||||||
|
total += len(batch)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := 0; i < 101; i++ {
|
||||||
|
go q.Enqueue(i)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
mu.Lock()
|
||||||
|
require.Equal(t, 100, total) // One is missing, stuck in the last batch!
|
||||||
|
require.Equal(t, 4, len(batches))
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBatchingQueue_WithTimeout(t *testing.T) {
|
||||||
|
q := util.NewBatchingQueue[int](25, 100*time.Millisecond)
|
||||||
|
batches, total := make([][]int, 0), 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
go func() {
|
||||||
|
for batch := range q.Dequeue() {
|
||||||
|
mu.Lock()
|
||||||
|
batches = append(batches, batch)
|
||||||
|
total += len(batch)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for i := 0; i < 101; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
time.Sleep(time.Duration(rand.Intn(700)) * time.Millisecond)
|
||||||
|
q.Enqueue(i)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
mu.Lock()
|
||||||
|
require.Equal(t, 101, total)
|
||||||
|
require.True(t, len(batches) > 4) // 101/25
|
||||||
|
require.True(t, len(batches) < 21)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
13785
web/package-lock.json
generated
13785
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,12 +30,12 @@
|
|||||||
"prefs_notifications_title": "Известия",
|
"prefs_notifications_title": "Известия",
|
||||||
"prefs_notifications_sound_title": "Звук при получаване",
|
"prefs_notifications_sound_title": "Звук при получаване",
|
||||||
"prefs_notifications_sound_no_sound": "Без звук",
|
"prefs_notifications_sound_no_sound": "Без звук",
|
||||||
"prefs_notifications_min_priority_title": "Минимален приоритет",
|
"prefs_notifications_min_priority_title": "Най-нисък приоритет",
|
||||||
"prefs_notifications_min_priority_any": "Всички",
|
"prefs_notifications_min_priority_any": "Всички",
|
||||||
"prefs_notifications_min_priority_low_and_higher": "Нисък приоритет и по-висок",
|
"prefs_notifications_min_priority_low_and_higher": "Нисък приоритет и по-висок",
|
||||||
"prefs_notifications_min_priority_default_and_higher": "Подразбиран приоритет и по-висок",
|
"prefs_notifications_min_priority_default_and_higher": "Подразбиран приоритет и по-висок",
|
||||||
"prefs_notifications_min_priority_high_and_higher": "Висок приоритет и по-висок",
|
"prefs_notifications_min_priority_high_and_higher": "Висок приоритет и по-висок",
|
||||||
"prefs_notifications_min_priority_max_only": "Само максимален приоритет",
|
"prefs_notifications_min_priority_max_only": "Само най-висок приоритет",
|
||||||
"prefs_notifications_delete_after_never": "Никога",
|
"prefs_notifications_delete_after_never": "Никога",
|
||||||
"prefs_users_add_button": "Добавяне",
|
"prefs_users_add_button": "Добавяне",
|
||||||
"prefs_users_dialog_password_label": "Парола",
|
"prefs_users_dialog_password_label": "Парола",
|
||||||
@@ -62,11 +62,11 @@
|
|||||||
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
|
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
|
||||||
"notifications_none_for_topic_title": "Липсват известия в темата",
|
"notifications_none_for_topic_title": "Липсват известия в темата",
|
||||||
"notifications_none_for_any_title": "Липсват известия",
|
"notifications_none_for_any_title": "Липсват известия",
|
||||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема, просто направете PUT или POST към адреса ѝ.",
|
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса й.",
|
||||||
"notifications_none_for_any_description": "За да изпратите известия в тема, просто направете PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
"notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
||||||
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като изпратите съобщения чрез метода PUT или POST ще ги получите тук.",
|
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.",
|
||||||
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
||||||
"publish_dialog_priority_min": "Мин. приоритет",
|
"publish_dialog_priority_min": "Най-нисък приоритет",
|
||||||
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл",
|
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл",
|
||||||
"publish_dialog_base_url_label": "Адрес на услугата",
|
"publish_dialog_base_url_label": "Адрес на услугата",
|
||||||
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
|
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
||||||
"publish_dialog_tags_label": "Етикети",
|
"publish_dialog_tags_label": "Етикети",
|
||||||
"publish_dialog_email_label": "Адрес на електронна поща",
|
"publish_dialog_email_label": "Адрес на електронна поща",
|
||||||
"publish_dialog_priority_max": "Макс. приоритет",
|
"publish_dialog_priority_max": "Най-висок приоритет",
|
||||||
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. warning, srv1-backup",
|
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. warning, srv1-backup",
|
||||||
"publish_dialog_click_label": "Адрес",
|
"publish_dialog_click_label": "Адрес",
|
||||||
"publish_dialog_topic_label": "Име на темата",
|
"publish_dialog_topic_label": "Име на темата",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"publish_dialog_attached_file_title": "Прикачен файл:",
|
"publish_dialog_attached_file_title": "Прикачен файл:",
|
||||||
"publish_dialog_attached_file_filename_placeholder": "Име на прикачения файл",
|
"publish_dialog_attached_file_filename_placeholder": "Име на прикачения файл",
|
||||||
"publish_dialog_drop_file_here": "Пуснете файла тук",
|
"publish_dialog_drop_file_here": "Пуснете файла тук",
|
||||||
"subscribe_dialog_subscribe_description": "Възможно е темите да не са защитени с парола, затова изберете име, което е трудно за отгатване. След като се абонирате, можете да изпращате известия по PUT или POST.",
|
"subscribe_dialog_subscribe_description": "Възможно е темите да не са защитени с парола, затова изберете име, което е трудно за отгатване. След като се абонирате, можете да изпращате известия чрез методите PUT или POST.",
|
||||||
"emoji_picker_search_placeholder": "Търсете емоция",
|
"emoji_picker_search_placeholder": "Търсете емоция",
|
||||||
"subscribe_dialog_subscribe_title": "Абониране за тема",
|
"subscribe_dialog_subscribe_title": "Абониране за тема",
|
||||||
"subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts",
|
"subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts",
|
||||||
@@ -140,10 +140,10 @@
|
|||||||
"prefs_notifications_sound_description_some": "При пристигане известията са съпроводени от звука „{{sound}}“",
|
"prefs_notifications_sound_description_some": "При пристигане известията са съпроводени от звука „{{sound}}“",
|
||||||
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
|
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
|
||||||
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
|
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
|
||||||
"priority_min": "минимален",
|
"priority_min": "най-нисък",
|
||||||
"priority_low": "нисък",
|
"priority_low": "нисък",
|
||||||
"priority_high": "висок",
|
"priority_high": "висок",
|
||||||
"priority_max": "максимален",
|
"priority_max": "най-висок",
|
||||||
"priority_default": "подразбиран",
|
"priority_default": "подразбиран",
|
||||||
"prefs_notifications_delete_after_one_week_description": "Известията се премахват автоматично след една седмица",
|
"prefs_notifications_delete_after_one_week_description": "Известията се премахват автоматично след една седмица",
|
||||||
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
|
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
"nav_button_muted": "Известията са заглушени",
|
"nav_button_muted": "Известията са заглушени",
|
||||||
"notifications_list": "Списък с известия",
|
"notifications_list": "Списък с известия",
|
||||||
"notifications_list_item": "Известие",
|
"notifications_list_item": "Известие",
|
||||||
"notifications_delete": "Изтриване",
|
"notifications_delete": "Премахване",
|
||||||
"notifications_mark_read": "Отбелязване като прочетено",
|
"notifications_mark_read": "Отбелязване като прочетено",
|
||||||
"nav_button_connecting": "свързване",
|
"nav_button_connecting": "свързване",
|
||||||
"message_bar_show_dialog": "Показване на диалога за публикуване",
|
"message_bar_show_dialog": "Показване на диалога за публикуване",
|
||||||
@@ -169,9 +169,9 @@
|
|||||||
"notifications_new_indicator": "Ново известие",
|
"notifications_new_indicator": "Ново известие",
|
||||||
"notifications_attachment_image": "Прикачено изображение",
|
"notifications_attachment_image": "Прикачено изображение",
|
||||||
"notifications_attachment_file_image": "файл на изображение",
|
"notifications_attachment_file_image": "файл на изображение",
|
||||||
"notifications_attachment_file_video": "файл на видео",
|
"notifications_attachment_file_video": "видео",
|
||||||
"notifications_attachment_file_audio": "файл на аудио",
|
"notifications_attachment_file_audio": "аудио",
|
||||||
"notifications_attachment_file_app": "Инсталационен файл на приложение за Android",
|
"notifications_attachment_file_app": "инсталационен файл на приложение за Android",
|
||||||
"notifications_attachment_file_document": "друг документ",
|
"notifications_attachment_file_document": "друг документ",
|
||||||
"publish_dialog_emoji_picker_show": "Избор на емоция",
|
"publish_dialog_emoji_picker_show": "Избор на емоция",
|
||||||
"publish_dialog_topic_reset": "Нулиране на тема",
|
"publish_dialog_topic_reset": "Нулиране на тема",
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
|
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
|
||||||
"prefs_notifications_sound_play": "Възпроизвеждане на избрания звук",
|
"prefs_notifications_sound_play": "Възпроизвеждане на избрания звук",
|
||||||
"publish_dialog_attach_reset": "Премахване на адреса на файла за прикачане",
|
"publish_dialog_attach_reset": "Премахване на адреса на файла за прикачане",
|
||||||
"prefs_users_delete_button": "Премахване на потребител",
|
"prefs_users_delete_button": "Премахване",
|
||||||
"prefs_users_table": "Таблица с потребители",
|
"prefs_users_table": "Таблица с потребители",
|
||||||
"prefs_users_edit_button": "Промяна на потребител",
|
"prefs_users_edit_button": "Промяна на потребител",
|
||||||
"error_boundary_unsupported_indexeddb_title": "Поверително разглеждане не се поддържа",
|
"error_boundary_unsupported_indexeddb_title": "Поверително разглеждане не се поддържа",
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
|
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
|
||||||
"subscribe_dialog_subscribe_use_another_label": "Use another server",
|
"subscribe_dialog_subscribe_use_another_label": "Use another server",
|
||||||
"subscribe_dialog_subscribe_base_url_label": "Service URL",
|
"subscribe_dialog_subscribe_base_url_label": "Service URL",
|
||||||
|
"subscribe_dialog_subscribe_button_generate_topic_name": "Generate name",
|
||||||
"subscribe_dialog_subscribe_button_cancel": "Cancel",
|
"subscribe_dialog_subscribe_button_cancel": "Cancel",
|
||||||
"subscribe_dialog_subscribe_button_subscribe": "Subscribe",
|
"subscribe_dialog_subscribe_button_subscribe": "Subscribe",
|
||||||
"subscribe_dialog_login_title": "Login required",
|
"subscribe_dialog_login_title": "Login required",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"message_bar_type_message": "Tapez un message ici",
|
"message_bar_type_message": "Tapez un message ici",
|
||||||
"notifications_attachment_open_button": "Ouvrir la pièce jointe",
|
"notifications_attachment_open_button": "Ouvrir la pièce jointe",
|
||||||
"notifications_attachment_link_expires": "le lien expire {{date}}",
|
"notifications_attachment_link_expires": "le lien expire {{date}}",
|
||||||
"message_bar_error_publishing": "Notification d'erreur de publication",
|
"message_bar_error_publishing": "Erreur lors de la publication de la notification",
|
||||||
"nav_button_all_notifications": "Toutes les notifications",
|
"nav_button_all_notifications": "Toutes les notifications",
|
||||||
"nav_button_settings": "Paramètres",
|
"nav_button_settings": "Paramètres",
|
||||||
"nav_button_documentation": "Documentation",
|
"nav_button_documentation": "Documentation",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"subscribe_dialog_login_title": "Connexion nécessaire",
|
"subscribe_dialog_login_title": "Connexion nécessaire",
|
||||||
"prefs_notifications_min_priority_low_and_higher": "Priorité basse et au-dessus",
|
"prefs_notifications_min_priority_low_and_higher": "Priorité basse et au-dessus",
|
||||||
"prefs_users_dialog_button_cancel": "Annuler",
|
"prefs_users_dialog_button_cancel": "Annuler",
|
||||||
"error_boundary_button_copy_stack_trace": "Copier la stack strace",
|
"error_boundary_button_copy_stack_trace": "Copier la trace d'appels",
|
||||||
"publish_dialog_attached_file_title": "Fichier joint :",
|
"publish_dialog_attached_file_title": "Fichier joint :",
|
||||||
"publish_dialog_checkbox_publish_another": "Publier un autre",
|
"publish_dialog_checkbox_publish_another": "Publier un autre",
|
||||||
"publish_dialog_attached_file_filename_placeholder": "Nom du fichier joint",
|
"publish_dialog_attached_file_filename_placeholder": "Nom du fichier joint",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"prefs_users_table_user_header": "Utilisateur",
|
"prefs_users_table_user_header": "Utilisateur",
|
||||||
"prefs_users_dialog_title_edit": "Éditer l'utilisateur",
|
"prefs_users_dialog_title_edit": "Éditer l'utilisateur",
|
||||||
"prefs_users_dialog_button_add": "Ajouter",
|
"prefs_users_dialog_button_add": "Ajouter",
|
||||||
"error_boundary_description": "Ceci ne devrait évidemment pas arriver. Désolé pour ça.<br/>Si vous avez une minute, merci de <githubLink>signaler ceci sur GitHub</githubLink>, ou faites-le nous savoir par <discordLink>Discord</discordLink> ou <matrixLink>Matric</matrixLink>.",
|
"error_boundary_description": "Ceci ne devrait évidemment pas arriver. Désolé pour ça.<br/>Si vous avez une minute, merci de <githubLink>signaler ceci sur GitHub</githubLink>, ou faites-le nous savoir par <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||||
"prefs_users_dialog_title_add": "Ajouter un utilisateur",
|
"prefs_users_dialog_title_add": "Ajouter un utilisateur",
|
||||||
"error_boundary_stack_trace": "Trace de pile d'appels",
|
"error_boundary_stack_trace": "Trace de pile d'appels",
|
||||||
"error_boundary_gathering_info": "Récupérer plus d'information…",
|
"error_boundary_gathering_info": "Récupérer plus d'information…",
|
||||||
|
|||||||
@@ -152,5 +152,40 @@
|
|||||||
"error_boundary_stack_trace": "Verem nyomkövetés",
|
"error_boundary_stack_trace": "Verem nyomkövetés",
|
||||||
"publish_dialog_title_topic": "A {{topic}} téma értesítése",
|
"publish_dialog_title_topic": "A {{topic}} téma értesítése",
|
||||||
"prefs_notifications_sound_description_some": "Az értesítéseket a(z) {{sound}} hang fogja jelezni",
|
"prefs_notifications_sound_description_some": "Az értesítéseket a(z) {{sound}} hang fogja jelezni",
|
||||||
"error_boundary_description": "Ennek nem szabadott volna megtörténnie. Nagyon sajnáljuk.<br/>Ha van egy perced, <githubLink>jelentsd be GitHubon</githubLink>, vagy tudasd velünk <discordLink>Discordon</discordLink>, vagy <matrixLink>Matrixon</matrixLink>."
|
"error_boundary_description": "Ennek nem szabadott volna megtörténnie. Nagyon sajnáljuk.<br/>Ha van egy perced, <githubLink>jelentsd be GitHubon</githubLink>, vagy tudasd velünk <discordLink>Discordon</discordLink>, vagy <matrixLink>Matrixon</matrixLink>.",
|
||||||
|
"action_bar_show_menu": "Menü mutatása",
|
||||||
|
"action_bar_toggle_mute": "Üzenetek némítása/bekapcsolása",
|
||||||
|
"notifications_list_item": "Értesítés",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "A ntfy web alkalmazás működéséhez szükséges az IndexedDB funkció, az ön böngészője nem támogatja az IndexedDB használatát privát böngészés közben.<br/><br/>Miközben privát mód sajnos nem lehetséges, szeretnénk értesíteni hogy magabiztosan használhatja normál módban mert a böngésző minden adatot az ön gépén tárol. Tovább tájékozódhat <githubLink>ezen a Github oldalon</githubLink>, vagy beszéljen velünk <discordLink>Discord-on</discordLink> vagy <matrixLink>Matrix-on</matrixLink>.",
|
||||||
|
"notifications_priority_x": "Prioritás {{prioritás}}",
|
||||||
|
"message_bar_show_dialog": "Küldött üzenetek megjelenítése",
|
||||||
|
"action_bar_logo_alt": "ntfy logó",
|
||||||
|
"action_bar_toggle_action_menu": "Tevékenységkezelő nyitása/zárása",
|
||||||
|
"message_bar_publish": "Üzenet küldése",
|
||||||
|
"nav_button_muted": "Értesítések némítva",
|
||||||
|
"nav_button_connecting": "csatlakozás",
|
||||||
|
"notifications_list": "Értesítés lista",
|
||||||
|
"notifications_mark_read": "Jelölés olvasottként",
|
||||||
|
"notifications_delete": "Törlés",
|
||||||
|
"notifications_new_indicator": "Új értesítés",
|
||||||
|
"notifications_attachment_image": "Csatolt kép",
|
||||||
|
"notifications_attachment_file_image": "Kép fájl",
|
||||||
|
"notifications_attachment_file_video": "Videó fájl",
|
||||||
|
"notifications_attachment_file_audio": "Hang fájl",
|
||||||
|
"notifications_attachment_file_app": "Android alkalmazás fájl",
|
||||||
|
"notifications_attachment_file_document": "egyéb dokumentum",
|
||||||
|
"publish_dialog_emoji_picker_show": "Emoji kiválasztása",
|
||||||
|
"publish_dialog_topic_reset": "Téma visszaállítása",
|
||||||
|
"publish_dialog_click_reset": "URL kattintás törlése",
|
||||||
|
"publish_dialog_email_reset": "Email továbbítás törlése",
|
||||||
|
"publish_dialog_attach_reset": "Csatolt URL törlése",
|
||||||
|
"publish_dialog_delay_reset": "Késleltetett kézbesítés törlése",
|
||||||
|
"publish_dialog_attached_file_remove": "Csatolt fájl törlése",
|
||||||
|
"emoji_picker_search_clear": "Keresés törlése",
|
||||||
|
"prefs_notifications_sound_play": "Kijelölt hang lejátszása",
|
||||||
|
"prefs_users_table": "Felhasználó táblázat",
|
||||||
|
"prefs_users_edit_button": "Felhasználó szerkesztése",
|
||||||
|
"prefs_users_delete_button": "Felhasználó törlése",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Privát böngészés nem támogatott",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "Szolgáltató URL"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
"publish_dialog_topic_label": "Emnenavn",
|
"publish_dialog_topic_label": "Emnenavn",
|
||||||
"prefs_notifications_delete_after_one_day_description": "Merknader slettes automatisk etter én dag",
|
"prefs_notifications_delete_after_one_day_description": "Merknader slettes automatisk etter én dag",
|
||||||
"notifications_click_copy_url_button": "Kopier lenke",
|
"notifications_click_copy_url_button": "Kopier lenke",
|
||||||
"error_boundary_title": "Oida. Ntfy krasjet.",
|
"error_boundary_title": "Oida, ntfy krasjet",
|
||||||
"publish_dialog_message_placeholder": "Skriv en melding her",
|
"publish_dialog_message_placeholder": "Skriv en melding her",
|
||||||
"publish_dialog_button_cancel": "Avbryt",
|
"publish_dialog_button_cancel": "Avbryt",
|
||||||
"prefs_notifications_min_priority_title": "Minimumsprioritet",
|
"prefs_notifications_min_priority_title": "Minimumsprioritet",
|
||||||
@@ -118,9 +118,74 @@
|
|||||||
"prefs_users_table_base_url_header": "Tjeneste-nettadresse",
|
"prefs_users_table_base_url_header": "Tjeneste-nettadresse",
|
||||||
"prefs_users_dialog_button_cancel": "Avbryt",
|
"prefs_users_dialog_button_cancel": "Avbryt",
|
||||||
"prefs_users_dialog_button_add": "Legg til",
|
"prefs_users_dialog_button_add": "Legg til",
|
||||||
"publish_dialog_chip_attach_url_label": "Legg ved fil per nettadresse",
|
"publish_dialog_chip_attach_url_label": "Legg til fil med nettadresse",
|
||||||
"publish_dialog_tags_placeholder": "Kommainndelt liste over etiketter, f.eks. advarsel, srv1-sikkerhetskopi",
|
"publish_dialog_tags_placeholder": "Kommainndelt liste over etiketter, f.eks. advarsel, srv1-sikkerhetskopi",
|
||||||
"prefs_notifications_sound_description_none": "Merknader er lydløse når de mottas",
|
"prefs_notifications_sound_description_none": "Merknader spiller ikke lyd når de mottas",
|
||||||
"subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_varsler",
|
"subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_varsler",
|
||||||
"prefs_notifications_min_priority_default_and_higher": "Forvalgt prioritet og høyere"
|
"prefs_notifications_min_priority_default_and_higher": "Forvalgt prioritet og høyere",
|
||||||
|
"notifications_no_subscriptions_title": "Det ser ut til at du ikke har noen abonnementer ennå.",
|
||||||
|
"publish_dialog_attachment_limits_file_and_quota_reached": "overskrider {{fileSizeLimit}} filgrense og kvote, {{remainingBytes}} gjenstår",
|
||||||
|
"publish_dialog_attachment_limits_file_reached": "overskrider filgrensen på {{fileSizeLimit}}",
|
||||||
|
"publish_dialog_title_label": "Tittel",
|
||||||
|
"publish_dialog_title_placeholder": "Varslingstittel, f.eks. Diskplassvarsel",
|
||||||
|
"publish_dialog_topic_placeholder": "Emnenavn, f.eks. halgeir_varsler",
|
||||||
|
"publish_dialog_chip_click_label": "Klikk URL",
|
||||||
|
"publish_dialog_chip_delay_label": "Forsink leveringen",
|
||||||
|
"publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se <docsLink>dokumentasjonen</docsLink>.",
|
||||||
|
"publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com",
|
||||||
|
"alert_grant_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.",
|
||||||
|
"alert_not_supported_description": "Varsler støttes ikke i nettleseren din.",
|
||||||
|
"notifications_attachment_file_app": "Android-app-fil",
|
||||||
|
"notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.",
|
||||||
|
"notifications_actions_http_request_title": "Send HTTP {{metode}} til {{url}}",
|
||||||
|
"notifications_none_for_any_description": "For å sende varsler til et emne, bare PUT eller POST til emne-URLen. Her er et eksempel som bruker et av emnene dine.",
|
||||||
|
"notifications_more_details": "For mer informasjon, sjekk ut <websiteLink>nettstedet</websiteLink> eller <docsLink>dokumentasjonen</docsLink>.",
|
||||||
|
"publish_dialog_attachment_limits_quota_reached": "overskrider kvoten, {{remainingBytes}} gjenstår",
|
||||||
|
"publish_dialog_click_reset": "Fjern klikk-URL",
|
||||||
|
"publish_dialog_delay_placeholder": "Forsinket levering, f.eks. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (bare på engelsk)",
|
||||||
|
"emoji_picker_search_clear": "Tøm søk",
|
||||||
|
"subscribe_dialog_subscribe_description": "Det kan hende emner ikke er passordsbeskyttet, så velg et navn som ikke er enkelt å gjette. Når du har abonnert kan du utføre PUT/POST av merknader.",
|
||||||
|
"publish_dialog_checkbox_publish_another": "Publiser enda en",
|
||||||
|
"subscribe_dialog_login_description": "Dette emnet er passordbeskyttet. Vennligst skriv inn brukernavn og passord for å abonnere.",
|
||||||
|
"prefs_notifications_sound_play": "Spill av valgt lyd",
|
||||||
|
"subscribe_dialog_error_user_not_authorized": "Bruker {{brukernavn}} ikke autorisert",
|
||||||
|
"prefs_users_delete_button": "Slett bruker",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "ntfy-nettappen trenger IndexedDB for å fungere, og nettleseren din støtter ikke IndexedDB i privat nettlesingsmodus.<br/><br/>Selv om dette er uheldig, gir det heller ikke så mye mening å bruke ntfy-nettappen i privat surfemodus uansett, fordi alt er lagret i nettleserlagringen. Du kan lese mer om det <githubLink>i denne GitHub-feilmeldingen</githubLink>, eller snakk med oss på <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"action_bar_show_menu": "Vis meny",
|
||||||
|
"action_bar_toggle_mute": "Aktiver/deaktiver notifikasjoner",
|
||||||
|
"prefs_notifications_min_priority_description_max": "Vis merknader hvis prioritet er 5 (maks.)",
|
||||||
|
"prefs_notifications_min_priority_any": "Hvilken som helst prioritet",
|
||||||
|
"prefs_notifications_min_priority_low_and_higher": "Lav prioritet og høyere",
|
||||||
|
"prefs_users_description": "Legg til/fjern brukere for dine beskyttede emner her. Vær oppmerksom på at brukernavn og passord er lagret i nettleserens lokale lagring.",
|
||||||
|
"error_boundary_description": "Dette skal åpenbart ikke skje. Beklager dette.<br/>Hvis du har et minutt, vennligst <githubLink>rapporter dette på GitHub</githubLink>, eller gi oss beskjed via <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"action_bar_logo_alt": "ntfy logo",
|
||||||
|
"message_bar_publish": "Publiser melding",
|
||||||
|
"action_bar_toggle_action_menu": "Åpne/lukk handlingsmeny",
|
||||||
|
"message_bar_show_dialog": "Vis publiseringsdialog",
|
||||||
|
"nav_button_muted": "Varsler dempet",
|
||||||
|
"nav_button_connecting": "kobler til",
|
||||||
|
"notifications_list": "Varslingsliste",
|
||||||
|
"notifications_list_item": "Varsling",
|
||||||
|
"notifications_mark_read": "Merk som lest",
|
||||||
|
"notifications_delete": "Slett",
|
||||||
|
"notifications_priority_x": "Prioritet {{prioritet}}",
|
||||||
|
"notifications_new_indicator": "Nytt varsel",
|
||||||
|
"notifications_attachment_image": "Vedlagt bilde",
|
||||||
|
"notifications_attachment_file_image": "bildefil",
|
||||||
|
"notifications_attachment_file_video": "videofil",
|
||||||
|
"notifications_attachment_file_audio": "lydfil",
|
||||||
|
"notifications_attachment_file_document": "annet dokument",
|
||||||
|
"notifications_actions_not_supported": "Handling støttes ikke i nettappen",
|
||||||
|
"notifications_none_for_topic_description": "For å sende varsler til dette emnet, bare PUT eller POST til emne-URLen.",
|
||||||
|
"publish_dialog_emoji_picker_show": "Velg emoji",
|
||||||
|
"publish_dialog_topic_reset": "Tilbakestill emne",
|
||||||
|
"publish_dialog_click_label": "Klikk URL",
|
||||||
|
"publish_dialog_email_reset": "Fjern videresending av e-post",
|
||||||
|
"publish_dialog_attach_reset": "Fjern URL vedlegg",
|
||||||
|
"publish_dialog_delay_reset": "Fjern forsinket levering",
|
||||||
|
"publish_dialog_attached_file_remove": "Fjern vedlagt fil",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "Tjeneste-URL",
|
||||||
|
"prefs_users_table": "Brukertabell",
|
||||||
|
"prefs_users_edit_button": "Rediger bruker",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Privat surfing støttes ikke"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"action_bar_settings": "Instellingen",
|
"action_bar_settings": "Instellingen",
|
||||||
"action_bar_send_test_notification": "Stuur test notificatie",
|
"action_bar_send_test_notification": "Verstuur testnotificatie.",
|
||||||
"action_bar_clear_notifications": "Wis alle notificaties",
|
"action_bar_clear_notifications": "Wis alle notificaties",
|
||||||
"message_bar_type_message": "Typ hier een bericht",
|
"message_bar_type_message": "Typ hier een bericht",
|
||||||
"action_bar_unsubscribe": "Afmelden",
|
"action_bar_unsubscribe": "Afmelden",
|
||||||
"message_bar_error_publishing": "Fout bij publiceren notificatie",
|
"message_bar_error_publishing": "Fout bij publiceren notificatie",
|
||||||
"nav_topics_title": "Geabonneerde onderwerpen",
|
"nav_topics_title": "Geabonneerde onderwerpen",
|
||||||
"nav_button_settings": "Instellingen",
|
"nav_button_settings": "Instellingen",
|
||||||
"alert_not_supported_description": "Notificaties worden niet ondersteund in je browser.",
|
"alert_not_supported_description": "Notificaties worden niet ondersteund door je browser.",
|
||||||
"notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.",
|
"notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.",
|
||||||
"publish_dialog_tags_label": "Tags",
|
"publish_dialog_tags_label": "Tags",
|
||||||
"publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen",
|
"publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen",
|
||||||
@@ -26,20 +26,20 @@
|
|||||||
"action_bar_show_menu": "Toon menu",
|
"action_bar_show_menu": "Toon menu",
|
||||||
"action_bar_logo_alt": "ntfy logo",
|
"action_bar_logo_alt": "ntfy logo",
|
||||||
"action_bar_toggle_mute": "Notificaties dempen/opheffen",
|
"action_bar_toggle_mute": "Notificaties dempen/opheffen",
|
||||||
"action_bar_toggle_action_menu": "Actie menu openen/sluiten",
|
"action_bar_toggle_action_menu": "Open/Sluit actiemenu",
|
||||||
"message_bar_show_dialog": "Toon publicatie venster",
|
"message_bar_show_dialog": "Toon publicatie venster",
|
||||||
"message_bar_publish": "Bericht publiceren",
|
"message_bar_publish": "Bericht publiceren",
|
||||||
"nav_button_all_notifications": "Alle notificaties",
|
"nav_button_all_notifications": "Alle notificaties",
|
||||||
"nav_button_documentation": "Documentatie",
|
"nav_button_documentation": "Documentatie",
|
||||||
"nav_button_publish_message": "Notificatie publiceren",
|
"nav_button_publish_message": "Notificatie publiceren",
|
||||||
"nav_button_subscribe": "Onderwerp abonneren",
|
"nav_button_subscribe": "Abonneer op onderwerp",
|
||||||
"nav_button_muted": "Notificaties gedempt",
|
"nav_button_muted": "Notificaties gedempt",
|
||||||
"nav_button_connecting": "verbinden",
|
"nav_button_connecting": "verbinden",
|
||||||
"alert_grant_title": "Notificaties zijn uitgeschakeld",
|
"alert_grant_title": "Notificaties zijn uitgeschakeld",
|
||||||
"alert_grant_description": "Geef je browser toestemming om meldingen weer te geven.",
|
"alert_grant_description": "Verleen je browser toestemming voor het weergeven van notificaties.",
|
||||||
"alert_grant_button": "Nu toestaan",
|
"alert_grant_button": "Nu toestaan",
|
||||||
"alert_not_supported_title": "Notificaties zijn niet ondersteund",
|
"alert_not_supported_title": "Notificaties zijn niet ondersteund",
|
||||||
"notifications_list": "Notificaties lijst",
|
"notifications_list": "Notificatielijst",
|
||||||
"notifications_list_item": "Notificatie",
|
"notifications_list_item": "Notificatie",
|
||||||
"notifications_mark_read": "Markeer als gelezen",
|
"notifications_mark_read": "Markeer als gelezen",
|
||||||
"notifications_delete": "Verwijder",
|
"notifications_delete": "Verwijder",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"notifications_attachment_file_audio": "audiobestand",
|
"notifications_attachment_file_audio": "audiobestand",
|
||||||
"notifications_attachment_file_app": "Android app bestand",
|
"notifications_attachment_file_app": "Android app bestand",
|
||||||
"notifications_attachment_file_document": "overig document",
|
"notifications_attachment_file_document": "overig document",
|
||||||
"notifications_click_copy_url_title": "URL naar klembord kopiëren",
|
"notifications_click_copy_url_title": "link URL naar klembord kopiëren",
|
||||||
"notifications_click_copy_url_button": "Link kopiëren",
|
"notifications_click_copy_url_button": "Link kopiëren",
|
||||||
"notifications_click_open_button": "Link openen",
|
"notifications_click_open_button": "Link openen",
|
||||||
"notifications_none_for_topic_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar de URL van het onderwerp.",
|
"notifications_none_for_topic_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar de URL van het onderwerp.",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"publish_dialog_title_no_topic": "Notificatie publiceren",
|
"publish_dialog_title_no_topic": "Notificatie publiceren",
|
||||||
"publish_dialog_progress_uploading": "Uploaden …",
|
"publish_dialog_progress_uploading": "Uploaden …",
|
||||||
"notifications_actions_open_url_title": "Ga naar {{url}}",
|
"notifications_actions_open_url_title": "Ga naar {{url}}",
|
||||||
"notifications_actions_not_supported": "Deze actie is niet ondersteund in de web applicatie",
|
"notifications_actions_not_supported": "Actie wordt niet ondersteund in de webapplicatie",
|
||||||
"notifications_actions_http_request_title": "Stuur HTTP {{method}} naar {{url}}",
|
"notifications_actions_http_request_title": "Stuur HTTP {{method}} naar {{url}}",
|
||||||
"notifications_none_for_topic_title": "Je hebt nog geen notificaties ontvangen voor dit onderwerp.",
|
"notifications_none_for_topic_title": "Je hebt nog geen notificaties ontvangen voor dit onderwerp.",
|
||||||
"publish_dialog_priority_low": "Lage prioriteit",
|
"publish_dialog_priority_low": "Lage prioriteit",
|
||||||
|
|||||||
191
web/public/static/langs/pt.json
Normal file
191
web/public/static/langs/pt.json
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"action_bar_clear_notifications": "Limpar todas as notificações",
|
||||||
|
"action_bar_send_test_notification": "Enviar notificação de teste",
|
||||||
|
"action_bar_unsubscribe": "Anular subscrição",
|
||||||
|
"action_bar_toggle_mute": "Ativa/Desativa notificações",
|
||||||
|
"action_bar_toggle_action_menu": "Abrir/fechar menu de ação",
|
||||||
|
"message_bar_type_message": "Escreva uma mensagem aqui",
|
||||||
|
"message_bar_error_publishing": "Erro ao publicar notificação",
|
||||||
|
"message_bar_publish": "Publicar mensagem",
|
||||||
|
"nav_topics_title": "Tópicos subscritos",
|
||||||
|
"nav_button_all_notifications": "Todas notificações",
|
||||||
|
"nav_button_settings": "Configurações",
|
||||||
|
"nav_button_documentation": "Documentação",
|
||||||
|
"nav_button_publish_message": "Publicar notificação",
|
||||||
|
"nav_button_subscribe": "Subscrever tópico",
|
||||||
|
"nav_button_muted": "Notificações desativadas",
|
||||||
|
"nav_button_connecting": "A ligar",
|
||||||
|
"alert_grant_title": "As notificações estão desativadas",
|
||||||
|
"alert_grant_description": "Conceder permissão ao seu navegador para mostrar notificações.",
|
||||||
|
"alert_not_supported_title": "Notificações não suportadas",
|
||||||
|
"notifications_list": "Lista de notificações",
|
||||||
|
"alert_not_supported_description": "As notificações não são suportadas pelo seu navegador.",
|
||||||
|
"notifications_list_item": "Notificação",
|
||||||
|
"notifications_mark_read": "Marcar como lido",
|
||||||
|
"notifications_delete": "Apagar",
|
||||||
|
"notifications_copied_to_clipboard": "Copiado para a área de transferência",
|
||||||
|
"notifications_tags": "Etiquetas",
|
||||||
|
"notifications_priority_x": "Prioridade {{priority}}",
|
||||||
|
"notifications_new_indicator": "Nova notificação",
|
||||||
|
"notifications_attachment_image": "Imagem anexada",
|
||||||
|
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
|
||||||
|
"notifications_attachment_copy_url_button": "Copiar URL",
|
||||||
|
"notifications_attachment_open_title": "Ir para {{url}}",
|
||||||
|
"notifications_attachment_link_expired": "a ligação de transferência expirou",
|
||||||
|
"notifications_attachment_open_button": "Abrir anexo",
|
||||||
|
"notifications_attachment_link_expires": "a ligação expira em {{date}}",
|
||||||
|
"notifications_attachment_file_image": "ficheiro de imagem",
|
||||||
|
"notifications_attachment_file_video": "ficheiro de vídeo",
|
||||||
|
"notifications_attachment_file_audio": "ficheiro de áudio",
|
||||||
|
"notifications_attachment_file_app": "ficheiro apk Android",
|
||||||
|
"notifications_attachment_file_document": "outros documentos",
|
||||||
|
"notifications_click_copy_url_title": "Copiar URL da ligação para a área de transferência",
|
||||||
|
"notifications_click_copy_url_button": "Copiar ligação",
|
||||||
|
"notifications_click_open_button": "Abrir ligação",
|
||||||
|
"notifications_actions_open_url_title": "Ir para {{url}}",
|
||||||
|
"notifications_actions_not_supported": "Ação não suportada na app web",
|
||||||
|
"notifications_actions_http_request_title": "Enviar HTTP {{method}} para {{url}}",
|
||||||
|
"notifications_none_for_topic_title": "Ainda não recebeu nenhuma notificação deste tópico.",
|
||||||
|
"notifications_none_for_topic_description": "Para enviar notificações deste tópico, basta usar os métodos PUT ou POST no URL do tópico.",
|
||||||
|
"notifications_none_for_any_title": "Ainda não recebeu nenhuma notificação.",
|
||||||
|
"notifications_none_for_any_description": "Para enviar notificações dum tópico, basta usar os métodos PUT ou POST no URL do tópico. Eis um exemplo usando um dos seus tópicos.",
|
||||||
|
"notifications_no_subscriptions_title": "Parece que ainda não tem nenhuma inscrição.",
|
||||||
|
"notifications_no_subscriptions_description": "Clique na ligação \"{{linktext}}\" para criar ou subscrever um tópico. Depois, poderá enviar mensagens via PUT ou POST e receberá notificações aqui.",
|
||||||
|
"notifications_example": "Exemplo",
|
||||||
|
"notifications_more_details": "Para mais informações, aceda ao <websiteLink>site</websiteLink> ou à <docsLink>documentação</docsLink>.",
|
||||||
|
"notifications_loading": "A carregar notificações…",
|
||||||
|
"publish_dialog_title_topic": "Publicar em {{topic}}",
|
||||||
|
"publish_dialog_title_no_topic": "Publicar notificação",
|
||||||
|
"publish_dialog_progress_uploading": "A enviar …",
|
||||||
|
"publish_dialog_progress_uploading_detail": "A enviar {{loaded}}/{{total}} ({{percent}}%)…",
|
||||||
|
"publish_dialog_message_published": "Notificação publicada",
|
||||||
|
"publish_dialog_attachment_limits_file_and_quota_reached": "excede limite de ficheiro de {{fileSizeLimit}} e cota, {{remainingBytes}} restante(s)",
|
||||||
|
"publish_dialog_attachment_limits_quota_reached": "excede a cota, {{remainingBytes}} restante(s)",
|
||||||
|
"publish_dialog_priority_min": "Prioridade mínima",
|
||||||
|
"publish_dialog_priority_low": "Prioridade baixa",
|
||||||
|
"publish_dialog_priority_default": "Prioridade padrão",
|
||||||
|
"publish_dialog_priority_high": "Prioridade alta",
|
||||||
|
"publish_dialog_base_url_label": "URL de serviço",
|
||||||
|
"publish_dialog_base_url_placeholder": "URL de serviço, por exemplo: https://exemplo.com",
|
||||||
|
"publish_dialog_topic_label": "Nome do tópico",
|
||||||
|
"publish_dialog_topic_placeholder": "Nome do tópico, por exemplo: \"avisos_do_filipe\"",
|
||||||
|
"publish_dialog_topic_reset": "Limpar tópico",
|
||||||
|
"publish_dialog_title_placeholder": "Título da notificação, por exemplo: \"Alerta de espaço em disco\"",
|
||||||
|
"publish_dialog_message_label": "Mensagem",
|
||||||
|
"publish_dialog_message_placeholder": "Escreva uma mensagem aqui",
|
||||||
|
"publish_dialog_tags_label": "Etiquetas",
|
||||||
|
"publish_dialog_tags_placeholder": "Lista de etiquetas, separadas por vírgula, por exemplo: aviso, srv1-backup",
|
||||||
|
"publish_dialog_priority_label": "Prioridade",
|
||||||
|
"publish_dialog_click_label": "URL de clique",
|
||||||
|
"publish_dialog_click_placeholder": "URL que é aberto quando a notificação é clicada",
|
||||||
|
"publish_dialog_click_reset": "Remover URL de clique",
|
||||||
|
"publish_dialog_email_label": "Email",
|
||||||
|
"publish_dialog_filename_placeholder": "Nome do ficheiro anexado",
|
||||||
|
"publish_dialog_email_placeholder": "Endereça para o qual encaminhar a notificação, por exemplo: filipe@exemplo.com",
|
||||||
|
"publish_dialog_email_reset": "Remover encaminhamento por email",
|
||||||
|
"publish_dialog_attach_label": "URL de anexo",
|
||||||
|
"publish_dialog_attach_placeholder": "Anexar ficheiro por URL, por exemplo: https://f-droid.org/F-Droid.apk",
|
||||||
|
"publish_dialog_attach_reset": "Remover URL de anexo",
|
||||||
|
"publish_dialog_filename_label": "Nome do ficheiro",
|
||||||
|
"publish_dialog_delay_label": "Atraso",
|
||||||
|
"publish_dialog_delay_placeholder": "Atraso na entrega, por exemplo \"{{{unixTimestamp}}\", \"{{relativeTime}}\", ou \"{{naturalLanguage}}\" (apenas em Inglês)",
|
||||||
|
"publish_dialog_other_features": "Outras funcionalidades:",
|
||||||
|
"publish_dialog_chip_click_label": "URL de clique",
|
||||||
|
"publish_dialog_chip_topic_label": "Alterar tópico",
|
||||||
|
"publish_dialog_details_examples_description": "Para obter exemplos e uma descrição detalhada de todas as funcionalidades de envio, consulte a <docsLink>documentação</docsLink>.",
|
||||||
|
"publish_dialog_button_cancel_sending": "Cancelar o envio",
|
||||||
|
"publish_dialog_attached_file_filename_placeholder": "Nome do ficheiro anexado",
|
||||||
|
"publish_dialog_attached_file_remove": "Remover ficheiro anexado",
|
||||||
|
"emoji_picker_search_clear": "Limpar pesquisa",
|
||||||
|
"subscribe_dialog_subscribe_description": "Os tópicos podem não ser protegidos por palavra-passe, por isso escolha um nome que não seja fácil de adivinhar. Uma vez subscrito, pode usar os métodos PUT/POST para publicar notificações.",
|
||||||
|
"subscribe_dialog_subscribe_use_another_label": "Usar outro servidor",
|
||||||
|
"subscribe_dialog_error_user_not_authorized": "Utilizador {{username}} não autorizado",
|
||||||
|
"prefs_notifications_min_priority_description_max": "Mostrar notificações se prioridade for 5 (máxima)",
|
||||||
|
"prefs_notifications_delete_after_one_week": "Após uma semana",
|
||||||
|
"prefs_notifications_delete_after_one_month": "Após um mês",
|
||||||
|
"prefs_notifications_delete_after_never_description": "As notificações nunca serão eliminadas automaticamente",
|
||||||
|
"prefs_notifications_delete_after_one_week_description": "As notificações serão eliminadas automaticamente após uma semana",
|
||||||
|
"prefs_notifications_delete_after_one_month_description": "As notificações serão eliminadas automaticamente após um mês",
|
||||||
|
"prefs_users_dialog_username_label": "Utilizador, por exemplo: \"filipe\"",
|
||||||
|
"prefs_users_dialog_password_label": "Palavra-passe",
|
||||||
|
"prefs_users_dialog_button_cancel": "Cancelar",
|
||||||
|
"prefs_users_dialog_button_add": "Adicionar",
|
||||||
|
"error_boundary_description": "Obviamente, isto não devia acontecer, lamentamos o sucedido.<br/>Se tiver um minuto, por favor <githubLink>relate isto no GitHub</githubLink>, ou informe-nos através de <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"error_boundary_stack_trace": "Erro (\"stack trace\")",
|
||||||
|
"error_boundary_gathering_info": "A recolher mais informações …",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Navegação anónima não suportada",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "A aplicação web ntfy necessita da \"IndexedDB\" para funcionar e o seu navegador não a suporta no modo de navegação privada.<br/><br/>Embora isso seja inconveniente, também não faz muito sentido usar a aplicação no modo de navegação privada de qualquer maneira, visto que tudo é guardado no armazenamento do navegador. Pode ler mais sobre isso <githubLink>nesta questão no GitHub</githubLink>, ou falar connosco por <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"action_bar_show_menu": "Mostrar menu",
|
||||||
|
"action_bar_logo_alt": "logótipo do ntfy",
|
||||||
|
"action_bar_settings": "Configurações",
|
||||||
|
"message_bar_show_dialog": "Mostrar caixa de publicação",
|
||||||
|
"alert_grant_button": "Conceder agora",
|
||||||
|
"publish_dialog_attachment_limits_file_reached": "excede o limite de ficheiro de {{fileSizeLimit}}",
|
||||||
|
"publish_dialog_emoji_picker_show": "Escolher emoji",
|
||||||
|
"publish_dialog_priority_max": "Prioridade máxima",
|
||||||
|
"publish_dialog_title_label": "Título",
|
||||||
|
"publish_dialog_delay_reset": "Remover atraso de entrega",
|
||||||
|
"publish_dialog_chip_email_label": "Encaminhar para email",
|
||||||
|
"publish_dialog_chip_attach_url_label": "Anexar ficheiro por URL",
|
||||||
|
"publish_dialog_chip_attach_file_label": "Anexar ficheiro local",
|
||||||
|
"publish_dialog_chip_delay_label": "Atraso de entrega",
|
||||||
|
"publish_dialog_button_cancel": "Cancelar",
|
||||||
|
"publish_dialog_button_send": "Enviar",
|
||||||
|
"publish_dialog_checkbox_publish_another": "Publicar outra",
|
||||||
|
"publish_dialog_attached_file_title": "Ficheiro anexado:",
|
||||||
|
"publish_dialog_drop_file_here": "Arraste o ficheiro para aqui",
|
||||||
|
"emoji_picker_search_placeholder": "Pesquisar emoji",
|
||||||
|
"subscribe_dialog_subscribe_title": "Subscrever tópico",
|
||||||
|
"subscribe_dialog_subscribe_topic_placeholder": "Nome do tópico, por exemplo: \"alertas_do_filipe\"",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL de serviço",
|
||||||
|
"subscribe_dialog_subscribe_button_cancel": "Cancelar",
|
||||||
|
"subscribe_dialog_subscribe_button_subscribe": "Subscrever",
|
||||||
|
"subscribe_dialog_login_title": "Autenticação necessária",
|
||||||
|
"subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.",
|
||||||
|
"subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"",
|
||||||
|
"subscribe_dialog_login_password_label": "Palavra-passe",
|
||||||
|
"subscribe_dialog_login_button_back": "Voltar",
|
||||||
|
"subscribe_dialog_login_button_login": "Autenticar",
|
||||||
|
"subscribe_dialog_error_user_anonymous": "anónimo",
|
||||||
|
"prefs_notifications_title": "Notificações",
|
||||||
|
"prefs_notifications_sound_title": "Som de notificações",
|
||||||
|
"prefs_notifications_sound_description_none": "Notificações não reproduzem nenhum som quando chegam",
|
||||||
|
"prefs_notifications_sound_description_some": "Notificações reproduzem som {{sound}} quando chegam",
|
||||||
|
"prefs_notifications_sound_no_sound": "Sem som",
|
||||||
|
"prefs_notifications_sound_play": "Reproduzir som selecionado",
|
||||||
|
"prefs_notifications_min_priority_title": "Prioridade mínima",
|
||||||
|
"prefs_notifications_min_priority_description_any": "A mostrar todas as notificações, independentemente da prioridade",
|
||||||
|
"prefs_notifications_min_priority_description_x_or_higher": "Mostrar notificações se prioridade for {{number}} ({{name}}) ou acima",
|
||||||
|
"prefs_notifications_min_priority_any": "Qualquer prioridade",
|
||||||
|
"prefs_notifications_min_priority_low_and_higher": "Prioridade baixa e acima",
|
||||||
|
"prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima",
|
||||||
|
"prefs_notifications_min_priority_high_and_higher": "Prioridade alta e acima",
|
||||||
|
"prefs_notifications_min_priority_max_only": "Apenas prioridade máxima",
|
||||||
|
"prefs_notifications_delete_after_title": "Eliminar notificações",
|
||||||
|
"prefs_notifications_delete_after_never": "Nunca",
|
||||||
|
"prefs_notifications_delete_after_three_hours": "Após três horas",
|
||||||
|
"prefs_notifications_delete_after_one_day": "Após um dia",
|
||||||
|
"prefs_notifications_delete_after_three_hours_description": "As notificações serão eliminadas automaticamente após três horas",
|
||||||
|
"prefs_notifications_delete_after_one_day_description": "As notificações serão eliminadas automaticamente após um dia",
|
||||||
|
"prefs_users_title": "Gerir utilizadores",
|
||||||
|
"prefs_users_description": "Adicionar/remover utilizadores aos seus tópicos protegidos. Note que o utilizador e palavra-passe são guardados no armazenamento local do navegador.",
|
||||||
|
"prefs_users_table": "Tabela de utilizadores",
|
||||||
|
"prefs_users_add_button": "Adicionar utilizador",
|
||||||
|
"prefs_users_edit_button": "Editar utilizador",
|
||||||
|
"prefs_users_delete_button": "Apagar utilizador",
|
||||||
|
"prefs_users_table_user_header": "Utilizador",
|
||||||
|
"prefs_users_table_base_url_header": "URL de serviço",
|
||||||
|
"prefs_users_dialog_title_add": "Adicionar utilizador",
|
||||||
|
"prefs_users_dialog_title_edit": "Editar utilizador",
|
||||||
|
"prefs_users_dialog_base_url_label": "URL de serviço, por exemplo: https://ntfy.sh",
|
||||||
|
"prefs_users_dialog_button_save": "Gravar",
|
||||||
|
"prefs_appearance_title": "Aparência",
|
||||||
|
"prefs_appearance_language_title": "Idioma",
|
||||||
|
"priority_min": "mínima",
|
||||||
|
"priority_low": "baixa",
|
||||||
|
"priority_default": "padrão",
|
||||||
|
"priority_high": "alta",
|
||||||
|
"priority_max": "máxima",
|
||||||
|
"error_boundary_title": "Oh não, o ntfy parou de funcionar",
|
||||||
|
"error_boundary_button_copy_stack_trace": "Copiar erro (\"stack trace\")"
|
||||||
|
}
|
||||||
11
web/public/static/langs/ro.json
Normal file
11
web/public/static/langs/ro.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"action_bar_show_menu": "Afișează meniu",
|
||||||
|
"action_bar_send_test_notification": "Trimite notificare de probă",
|
||||||
|
"action_bar_clear_notifications": "Șterge toate notificările",
|
||||||
|
"action_bar_settings": "Setări",
|
||||||
|
"action_bar_unsubscribe": "Dezabonare",
|
||||||
|
"action_bar_logo_alt": "logo-ul ntfy",
|
||||||
|
"action_bar_toggle_mute": "Oprire/activare notificări",
|
||||||
|
"message_bar_type_message": "Scrie un mesaj aici",
|
||||||
|
"message_bar_error_publishing": "Eroare la publicarea notificării"
|
||||||
|
}
|
||||||
@@ -41,5 +41,11 @@
|
|||||||
"alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.",
|
"alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.",
|
||||||
"alert_not_supported_description": "Notiser stöds inte i din webbläsare.",
|
"alert_not_supported_description": "Notiser stöds inte i din webbläsare.",
|
||||||
"notifications_mark_read": "Markera som läst",
|
"notifications_mark_read": "Markera som läst",
|
||||||
"notifications_attachment_file_video": "video fil"
|
"notifications_attachment_file_video": "video fil",
|
||||||
|
"notifications_click_copy_url_button": "Kopiera länk",
|
||||||
|
"notifications_click_open_button": "Öppna länk",
|
||||||
|
"notifications_actions_open_url_title": "Gå till {{url}}",
|
||||||
|
"notifications_none_for_any_title": "Du har inte fått några notiser.",
|
||||||
|
"notifications_example": "Exempel",
|
||||||
|
"notifications_loading": "Laddar notiser …"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,15 @@
|
|||||||
"notifications_list_item": "通知",
|
"notifications_list_item": "通知",
|
||||||
"notifications_mark_read": "標示已讀",
|
"notifications_mark_read": "標示已讀",
|
||||||
"notifications_attachment_image": "附加圖片",
|
"notifications_attachment_image": "附加圖片",
|
||||||
"notifications_attachment_copy_url_title": "複製附件URL到剪貼板",
|
"notifications_attachment_copy_url_title": "複製附件 URL 到剪貼簿",
|
||||||
"notifications_attachment_copy_url_button": "複製URL",
|
"notifications_attachment_copy_url_button": "複製 URL",
|
||||||
"notifications_attachment_open_title": "前往 {{url}}",
|
"notifications_attachment_open_title": "前往 {{url}}",
|
||||||
"notifications_attachment_open_button": "開啟附件",
|
"notifications_attachment_open_button": "開啟附件",
|
||||||
"notifications_attachment_link_expired": "下載連結已過期",
|
"notifications_attachment_link_expired": "下載連結已過期",
|
||||||
"notifications_attachment_file_video": "影片檔案",
|
"notifications_attachment_file_video": "影片檔案",
|
||||||
"notifications_attachment_file_app": "Android 應用程式檔案",
|
"notifications_attachment_file_app": "Android 應用程式檔案",
|
||||||
"notifications_attachment_file_document": "其他文件",
|
"notifications_attachment_file_document": "其他文件",
|
||||||
"notifications_click_copy_url_title": "複製連結URL到剪貼板",
|
"notifications_click_copy_url_title": "複製連結 URL 到剪貼板",
|
||||||
"notifications_click_copy_url_button": "複製連結",
|
"notifications_click_copy_url_button": "複製連結",
|
||||||
"notifications_click_open_button": "開啟連結",
|
"notifications_click_open_button": "開啟連結",
|
||||||
"notifications_actions_not_supported": "網頁程式無法支援該動作",
|
"notifications_actions_not_supported": "網頁程式無法支援該動作",
|
||||||
@@ -27,16 +27,16 @@
|
|||||||
"notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
|
"notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
|
||||||
"notifications_none_for_any_title": "尚未收到任何通知。",
|
"notifications_none_for_any_title": "尚未收到任何通知。",
|
||||||
"action_bar_settings": "設定",
|
"action_bar_settings": "設定",
|
||||||
"action_bar_send_test_notification": "寄送測試通知",
|
"action_bar_send_test_notification": "發送測試通知",
|
||||||
"action_bar_clear_notifications": "清除所有通知",
|
"action_bar_clear_notifications": "清除所有通知",
|
||||||
"action_bar_show_menu": "顯示選單",
|
"action_bar_show_menu": "顯示選單",
|
||||||
"nav_button_documentation": "文件",
|
"nav_button_documentation": "文件",
|
||||||
"nav_button_publish_message": "發布通知",
|
"nav_button_publish_message": "發佈通知",
|
||||||
"nav_button_muted": "通知已靜音",
|
"nav_button_muted": "通知已靜音",
|
||||||
"notifications_copied_to_clipboard": "複製到剪貼板",
|
"notifications_copied_to_clipboard": "已複製到剪貼簿",
|
||||||
"message_bar_publish": "發布訊息",
|
"message_bar_publish": "發佈訊息",
|
||||||
"message_bar_show_dialog": "顯示發布對話筐",
|
"message_bar_show_dialog": "顯示發佈對話框",
|
||||||
"message_bar_error_publishing": "無法發布通知",
|
"message_bar_error_publishing": "發佈通知時發生錯誤",
|
||||||
"nav_topics_title": "訂閱主題",
|
"nav_topics_title": "訂閱主題",
|
||||||
"nav_button_all_notifications": "所有通知",
|
"nav_button_all_notifications": "所有通知",
|
||||||
"nav_button_settings": "設定",
|
"nav_button_settings": "設定",
|
||||||
@@ -50,7 +50,36 @@
|
|||||||
"notifications_new_indicator": "新通知",
|
"notifications_new_indicator": "新通知",
|
||||||
"notifications_attachment_file_audio": "聲音檔案",
|
"notifications_attachment_file_audio": "聲音檔案",
|
||||||
"notifications_delete": "刪除",
|
"notifications_delete": "刪除",
|
||||||
"notifications_attachment_link_expires": "連結已過期 {{date}}",
|
"notifications_attachment_link_expires": "連結在 {{date}} 過期",
|
||||||
"notifications_attachment_file_image": "圖片檔案",
|
"notifications_attachment_file_image": "圖片檔案",
|
||||||
"notifications_actions_open_url_title": "前往 {{url}}"
|
"notifications_actions_open_url_title": "前往 {{url}}",
|
||||||
|
"notifications_no_subscriptions_title": "你尚未有任何訂閱。",
|
||||||
|
"notifications_example": "範例",
|
||||||
|
"notifications_more_details": "你可以在 <websiteLink>ntfy 網站</websiteLink>或者<docsLink>技術文件</docsLink>中查看更多資訊。",
|
||||||
|
"notifications_loading": "載入中…",
|
||||||
|
"publish_dialog_title_topic": "發佈到 {{topic}}",
|
||||||
|
"publish_dialog_title_no_topic": "發佈通知",
|
||||||
|
"publish_dialog_progress_uploading": "上傳中…",
|
||||||
|
"publish_dialog_priority_label": "優先度",
|
||||||
|
"publish_dialog_email_label": "電郵地址",
|
||||||
|
"publish_dialog_filename_label": "檔案名稱",
|
||||||
|
"publish_dialog_button_cancel": "取消",
|
||||||
|
"publish_dialog_button_send": "傳送",
|
||||||
|
"publish_dialog_button_cancel_sending": "取消傳送",
|
||||||
|
"subscribe_dialog_subscribe_button_cancel": "取消",
|
||||||
|
"subscribe_dialog_subscribe_button_subscribe": "訂閱",
|
||||||
|
"emoji_picker_search_clear": "清除",
|
||||||
|
"subscribe_dialog_login_password_label": "密碼",
|
||||||
|
"subscribe_dialog_login_button_back": "返回",
|
||||||
|
"subscribe_dialog_login_button_login": "登入",
|
||||||
|
"prefs_notifications_delete_after_never": "從不",
|
||||||
|
"prefs_users_add_button": "新增使用者",
|
||||||
|
"prefs_users_dialog_password_label": "密碼",
|
||||||
|
"prefs_users_dialog_title_add": "新增使用者",
|
||||||
|
"prefs_users_dialog_button_save": "儲存",
|
||||||
|
"prefs_users_dialog_button_cancel": "取消",
|
||||||
|
"error_boundary_title": "歐買尬,ntfy 壞掉了",
|
||||||
|
"notifications_none_for_any_description": "要開始發送通知到一個主題,只需要對主題 URL 發送 HTTP PUT 或者 POST,例如:",
|
||||||
|
"notifications_no_subscriptions_description": "點選 「{{linktext}}」 連結以建立或訂閱主題。完成後,你就可以使用 HTTP PUT 或者 POST 發送通知到這裡了!",
|
||||||
|
"error_boundary_description": "很抱歉 ntfy 發生錯誤了。<br/>如果你有時間,煩請到<githubLink> Github </githubLink>回報錯誤,或者到<discordLink> Discord </discordLink>或者<matrixLink> Matrix 聊天室</matrixLink>裡面告訴我們。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export const unmatchedTags = (tags) => {
|
|||||||
else return tags.filter(tag => !(tag in emojis));
|
else return tags.filter(tag => !(tag in emojis));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const maybeWithBasicAuth = (headers, user) => {
|
export const maybeWithBasicAuth = (headers, user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
|
headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
|
||||||
@@ -241,3 +240,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
|||||||
yield chunk.substr(startIndex); // last line didn't end in a newline char
|
yield chunk.substr(startIndex); // last line didn't end in a newline char
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const randomAlphanumericString = (len) => {
|
||||||
|
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let id = "";
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
id += alphabet[(Math.random() * alphabet.length) | 0];
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|||||||
@@ -463,6 +463,7 @@ const Language = () => {
|
|||||||
<MenuItem value="nl">Nederlands</MenuItem>
|
<MenuItem value="nl">Nederlands</MenuItem>
|
||||||
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
||||||
<MenuItem value="uk">Українська</MenuItem>
|
<MenuItem value="uk">Українська</MenuItem>
|
||||||
|
<MenuItem value="pt">Português</MenuItem>
|
||||||
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
|
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
|
||||||
<MenuItem value="pl">Polski</MenuItem>
|
<MenuItem value="pl">Polski</MenuItem>
|
||||||
<MenuItem value="ru">Русский</MenuItem>
|
<MenuItem value="ru">Русский</MenuItem>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||||
import theme from "./theme";
|
import theme from "./theme";
|
||||||
import api from "../app/Api";
|
import api from "../app/Api";
|
||||||
import {topicUrl, validTopic, validUrl} from "../app/utils";
|
import {randomAlphanumericString, topicUrl, validTopic, validUrl} from "../app/utils";
|
||||||
import userManager from "../app/UserManager";
|
import userManager from "../app/UserManager";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
import poller from "../app/Poller";
|
import poller from "../app/Poller";
|
||||||
@@ -104,21 +104,26 @@ const SubscribePage = (props) => {
|
|||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
{t("subscribe_dialog_subscribe_description")}
|
{t("subscribe_dialog_subscribe_description")}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
<TextField
|
<div style={{display: 'flex'}} role="row">
|
||||||
autoFocus
|
<TextField
|
||||||
margin="dense"
|
autoFocus
|
||||||
id="topic"
|
margin="dense"
|
||||||
placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
|
id="topic"
|
||||||
value={props.topic}
|
placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
|
||||||
onChange={ev => props.setTopic(ev.target.value)}
|
value={props.topic}
|
||||||
type="text"
|
onChange={ev => props.setTopic(ev.target.value)}
|
||||||
fullWidth
|
type="text"
|
||||||
variant="standard"
|
fullWidth
|
||||||
inputProps={{
|
variant="standard"
|
||||||
maxLength: 64,
|
inputProps={{
|
||||||
"aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
|
maxLength: 64,
|
||||||
}}
|
"aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => {props.setTopic(randomAlphanumericString(16))}} style={{flexShrink: "0", marginTop: "0.5em"}}>
|
||||||
|
{t("subscribe_dialog_subscribe_button_generate_topic_name")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
sx={{pt: 1}}
|
sx={{pt: 1}}
|
||||||
control={
|
control={
|
||||||
|
|||||||
Reference in New Issue
Block a user