Compare commits
342 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7e6e57fe | ||
|
|
0b78d3173d | ||
|
|
92d7e5c58a | ||
|
|
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 | ||
|
|
4bcbea32ab | ||
|
|
1b96444401 | ||
|
|
651c701b9d | ||
|
|
019e69ec85 | ||
|
|
7470ffde4f | ||
|
|
2361e556e9 | ||
|
|
fea9d10ed2 | ||
|
|
9155c49571 | ||
|
|
baa15110ff | ||
|
|
5fefefc50f | ||
|
|
958b0e0d26 | ||
|
|
49732bcb3d | ||
|
|
ce43daaa73 | ||
|
|
325eca470e | ||
|
|
8988f04fb3 | ||
|
|
83118dfc64 | ||
|
|
29fbf73da0 | ||
|
|
5e1c60091f | ||
|
|
147cc1971b | ||
|
|
4a898f5b89 | ||
|
|
162dc1dbfa | ||
|
|
303cb3f8f8 | ||
|
|
4b9bb0ff2a | ||
|
|
cb247f3317 | ||
|
|
3972b2763d | ||
|
|
e2dd5f3da0 | ||
|
|
0b3173ada9 | ||
|
|
f3174f822f | ||
|
|
37ed7ef7bc | ||
|
|
cc3b9b89bf | ||
|
|
93cacc3a53 | ||
|
|
0234041e1e | ||
|
|
2fb7523d06 | ||
|
|
95e087390f | ||
|
|
0821b8a25f | ||
|
|
e320fef0c3 | ||
|
|
e874f66572 | ||
|
|
72d568db11 | ||
|
|
88e80aa252 | ||
|
|
2b823556b3 | ||
|
|
38441a2bd3 | ||
|
|
93fe19b4ed | ||
|
|
67d0fdd9b6 | ||
|
|
63f3774c41 | ||
|
|
7120dd5a27 | ||
|
|
c44c1aa237 | ||
|
|
5997761051 | ||
|
|
a17c294081 | ||
|
|
78d36a6d1d | ||
|
|
afac9ad5d3 | ||
|
|
2c59fd8bdb | ||
|
|
147774761b | ||
|
|
62cd517223 | ||
|
|
29b6517257 | ||
|
|
8b9cef7044 | ||
|
|
0e021dc1ce | ||
|
|
22c90d557b | ||
|
|
c02f7dd14d | ||
|
|
fb64d03479 | ||
|
|
956e092413 | ||
|
|
9d85cfa062 | ||
|
|
be1ba135e6 | ||
|
|
2d39ae1d1a | ||
|
|
df9fe7f8d0 | ||
|
|
1d6b792197 | ||
|
|
aaa6de9f26 | ||
|
|
536b5d364a | ||
|
|
87f112c9b7 | ||
|
|
cf370bfdda | ||
|
|
0d46bfa76e | ||
|
|
5b8372d260 | ||
|
|
ec72df046f | ||
|
|
947a4c1e74 | ||
|
|
9848bc7429 | ||
|
|
e54aeb357c | ||
|
|
d989ba0ab0 | ||
|
|
838543f489 | ||
|
|
fae5b38f67 | ||
|
|
6c3fe686be | ||
|
|
5dacd6f2c7 | ||
|
|
4ca721bb1f | ||
|
|
5d9702b10b | ||
|
|
85eb9160d8 | ||
|
|
322abf4bdf | ||
|
|
f007232520 | ||
|
|
dfec18be3d | ||
|
|
b7a18bd181 | ||
|
|
ce392de0a8 | ||
|
|
383ae66a48 | ||
|
|
24940f8a3b | ||
|
|
54eae00774 | ||
|
|
1b82beea6e | ||
|
|
cb8b3e54f6 | ||
|
|
d48619a940 | ||
|
|
ca5ec53261 | ||
|
|
819c896d40 | ||
|
|
dd689fd4a6 | ||
|
|
cbc912d1e3 | ||
|
|
16ad94441b | ||
|
|
1672322fc1 | ||
|
|
bc5060b218 | ||
|
|
4edc625331 | ||
|
|
3b29294679 | ||
|
|
511d3f6aaf | ||
|
|
de2ca33700 | ||
|
|
c2382d29a1 | ||
|
|
a70ee81d3b | ||
|
|
bb2f9cbe2b | ||
|
|
e1eca2323e | ||
|
|
9e15a4cfe2 | ||
|
|
e63b521bc9 | ||
|
|
4d6d6f7204 | ||
|
|
e0ad926ce9 | ||
|
|
04e91a1616 | ||
|
|
5014bba0b3 | ||
|
|
eaf3e83e72 | ||
|
|
bddde5c637 | ||
|
|
b15ecd785e | ||
|
|
f8c9945cc4 | ||
|
|
0fc8dee9a9 | ||
|
|
f01576e40d | ||
|
|
ea669c75a3 | ||
|
|
4abd0e290a | ||
|
|
bcda08a01c | ||
|
|
60043f14ea | ||
|
|
d3cfa3456c | ||
|
|
903ba8df4d | ||
|
|
46fcbdb827 | ||
|
|
419bfecd6f | ||
|
|
a9019131cf | ||
|
|
5e0e8e7db0 | ||
|
|
f0f4de2719 | ||
|
|
61d5293ba0 | ||
|
|
fd21d2f4ce | ||
|
|
e6b07e22a8 | ||
|
|
b117c217e4 | ||
|
|
1e823b4f89 | ||
|
|
36e805828e | ||
|
|
b37b3d97ad | ||
|
|
4446795dad | ||
|
|
ed4cc86c5c | ||
|
|
6476978a2e | ||
|
|
23a127d20b | ||
|
|
ae1fb74ac6 | ||
|
|
38c3b1fbf7 | ||
|
|
42c0dbab65 | ||
|
|
97a55babe1 | ||
|
|
c84d10a6df | ||
|
|
f7f72f44a1 | ||
|
|
f54dce4c3f | ||
|
|
cee46044cd | ||
|
|
58e782b475 | ||
|
|
601f01bc49 | ||
|
|
9dc19f1d07 | ||
|
|
4ea1e23361 | ||
|
|
2fb93b1eb7 | ||
|
|
eed3e28790 | ||
|
|
e60e770419 | ||
|
|
62c8cafff9 | ||
|
|
5181acdd7c | ||
|
|
6db2908d69 | ||
|
|
925017f040 | ||
|
|
6935d83ab3 | ||
|
|
54f762558a | ||
|
|
a22fd4db1c | ||
|
|
3f85e0a0c8 | ||
|
|
b0d58a618e | ||
|
|
29a248701f | ||
|
|
ec64b412a8 | ||
|
|
f5f9758a50 | ||
|
|
0d5362f0e4 | ||
|
|
fb7a2455fa | ||
|
|
85b2a674ae | ||
|
|
4277d6e4a6 | ||
|
|
3aa0eb7d1d | ||
|
|
ec3e6e902e | ||
|
|
9d0231ea07 | ||
|
|
08d717afbf | ||
|
|
9e151253e3 | ||
|
|
e4c760f1de | ||
|
|
4c566c9f31 | ||
|
|
a498e43d61 | ||
|
|
613d5d554f | ||
|
|
f6a42e7dcd | ||
|
|
8956837443 | ||
|
|
28975e9433 | ||
|
|
206beb31c4 | ||
|
|
38e61d6a99 | ||
|
|
3c5a10de17 | ||
|
|
99886d7f66 | ||
|
|
04f2535e92 | ||
|
|
d519fd999b | ||
|
|
cbcd0e3f0d | ||
|
|
9bcec02f8c | ||
|
|
88a77cb132 | ||
|
|
10a9aca2a1 | ||
|
|
3e53d8a2c7 | ||
|
|
d8ce68b2cb | ||
|
|
dd6af3b8f2 | ||
|
|
e874f3516e | ||
|
|
bf8077626e | ||
|
|
8532b5b7ea | ||
|
|
ed1673beed | ||
|
|
89316487e3 | ||
|
|
9f358d4793 | ||
|
|
e8953aea3b | ||
|
|
95bd876be2 | ||
|
|
bd6f3ca2e8 | ||
|
|
aee4074792 | ||
|
|
4d6c147f24 | ||
|
|
691a77370e | ||
|
|
d09afd8b60 | ||
|
|
2d26a990a9 | ||
|
|
f134bc6dcd | ||
|
|
50a830c360 | ||
|
|
ae3715222f | ||
|
|
eb841604c7 | ||
|
|
30c8d6b02b | ||
|
|
b840d7d5f4 | ||
|
|
20f835df50 | ||
|
|
bac5e1fa84 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: [binwiederhier]
|
||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '17'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@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
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '17'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -3,7 +3,7 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
steps:
|
||||
-
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '17'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@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
|
||||
@@ -64,9 +64,7 @@ builds:
|
||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
hooks:
|
||||
post:
|
||||
- upx "{{ .Path }}" # apt install upx
|
||||
# No "upx" for Windows to hopefully avoid Virus warnings
|
||||
-
|
||||
id: ntfy_darwin_all
|
||||
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
|
||||
|
||||
7
Makefile
7
Makefile
@@ -1,5 +1,6 @@
|
||||
MAKEFLAGS := --jobs=1
|
||||
VERSION := $(shell git describe --tag)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
|
||||
.PHONY:
|
||||
|
||||
@@ -169,7 +170,7 @@ cli-linux-server: cli-deps-static-sites
|
||||
-o dist/ntfy_linux_server/ntfy \
|
||||
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||
-ldflags \
|
||||
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||
|
||||
cli-darwin-server: cli-deps-static-sites
|
||||
# This is a target to build the CLI (including the server) manually.
|
||||
@@ -179,7 +180,7 @@ cli-darwin-server: cli-deps-static-sites
|
||||
-o dist/ntfy_darwin_server/ntfy \
|
||||
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||
-ldflags \
|
||||
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||
|
||||
cli-client: cli-deps-static-sites
|
||||
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
||||
@@ -189,7 +190,7 @@ cli-client: cli-deps-static-sites
|
||||
-o dist/ntfy_client/ntfy \
|
||||
-tags noserver \
|
||||
-ldflags \
|
||||
"-X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||
"-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||
|
||||
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
||||
|
||||
|
||||
80
README.md
80
README.md
@@ -9,7 +9,10 @@
|
||||
[](https://discord.gg/cT7ECsZj9w)
|
||||
[](https://matrix.to/#/#ntfy:matrix.org)
|
||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||
[](https://www.reddit.com/r/ntfy/)
|
||||
[](https://ntfy.statuspage.io/)
|
||||
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||
|
||||
|
||||
**ntfy** (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**.
|
||||
@@ -33,10 +36,15 @@ I run a free version of it at **[ntfy.sh](https://ntfy.sh)**. There's also an [o
|
||||
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
|
||||
[Building](https://ntfy.sh/docs/develop/)
|
||||
|
||||
## Chat
|
||||
You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org)
|
||||
(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information
|
||||
[on my website](https://heckel.io/about).
|
||||
## Chat / forum
|
||||
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
|
||||
works best for you:
|
||||
|
||||
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
||||
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
||||
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
|
||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
|
||||
|
||||
## Announcements / beta testers
|
||||
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
|
||||
@@ -44,21 +52,77 @@ 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).
|
||||
|
||||
## Contributing
|
||||
I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out
|
||||
the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app.
|
||||
Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/ntfy/">
|
||||
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Sponsors
|
||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
||||
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
||||
appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
||||
|
||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
|
||||
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
|
||||
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
|
||||
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
|
||||
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
|
||||
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
|
||||
<a href="https://github.com/Lexevolution"><img src="https://github.com/Lexevolution.png" width="40px" /></a>
|
||||
<a href="https://github.com/johnnyip"><img src="https://github.com/johnnyip.png" width="40px" /></a>
|
||||
<a href="https://github.com/JonDerThan"><img src="https://github.com/JonDerThan.png" width="40px" /></a>
|
||||
<a href="https://github.com/12nick12"><img src="https://github.com/12nick12.png" width="40px" /></a>
|
||||
<a href="https://github.com/eanplatter"><img src="https://github.com/eanplatter.png" width="40px" /></a>
|
||||
<a href="https://github.com/fnoelscher"><img src="https://github.com/fnoelscher.png" width="40px" /></a>
|
||||
<a href="https://github.com/bnorick"><img src="https://github.com/bnorick.png" width="40px" /></a>
|
||||
<a href="https://github.com/snh"><img src="https://github.com/snh.png" width="40px" /></a>
|
||||
<a href="https://github.com/hen-x"><img src="https://github.com/hen-x.png" width="40px" /></a>
|
||||
<a href="https://github.com/JamieGoodson"><img src="https://github.com/JamieGoodson.png" width="40px" /></a>
|
||||
<a href="https://github.com/cremesk"><img src="https://github.com/cremesk.png" width="40px" /></a>
|
||||
<a href="https://github.com/dangowans"><img src="https://github.com/dangowans.png" width="40px" /></a>
|
||||
<a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
|
||||
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
|
||||
<a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
|
||||
<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
|
||||
<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
|
||||
<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
|
||||
<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
|
||||
<a href="https://github.com/tonyakwei"><img src="https://github.com/tonyakwei.png" width="40px" /></a>
|
||||
<a href="https://github.com/crosbyh"><img src="https://github.com/crosbyh.png" width="40px" /></a>
|
||||
<a href="https://github.com/mdlnr"><img src="https://github.com/mdlnr.png" width="40px" /></a>
|
||||
<a href="https://github.com/p-samuel"><img src="https://github.com/p-samuel.png" width="40px" /></a>
|
||||
<a href="https://github.com/zugaldia"><img src="https://github.com/zugaldia.png" width="40px" /></a>
|
||||
<a href="https://github.com/NathanSweet"><img src="https://github.com/NathanSweet.png" width="40px" /></a>
|
||||
<a href="https://github.com/msdeibel"><img src="https://github.com/msdeibel.png" width="40px" /></a>
|
||||
<a href="https://github.com/ksurl"><img src="https://github.com/ksurl.png" width="40px" /></a>
|
||||
<a href="https://github.com/CodingTimeDEV"><img src="https://github.com/CodingTimeDEV.png" width="40px" /></a>
|
||||
<a href="https://github.com/Terrormixer3000"><img src="https://github.com/Terrormixer3000.png" width="40px" /></a>
|
||||
<a href="https://github.com/voroskoi"><img src="https://github.com/voroskoi.png" width="40px" /></a>
|
||||
|
||||
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://www.digitalocean.com/) for supporting the project with $60/yr:
|
||||
|
||||
<a href="https://www.digitalocean.com/"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||
|
||||
## Code of Conduct
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
|
||||
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
|
||||
|
||||
_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
||||
|
||||
## License
|
||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
|
||||
|
||||
Third party libraries and resources:
|
||||
* [github.com/urfave/cli/v2](https://github.com/urfave/cli/v2) (MIT) is used to drive the CLI
|
||||
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
|
||||
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
|
||||
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
|
||||
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
|
||||
|
||||
@@ -47,6 +47,7 @@ type Message struct { // TODO combine with server.message
|
||||
Priority int
|
||||
Tags []string
|
||||
Click string
|
||||
Icon string
|
||||
Attachment *Attachment
|
||||
|
||||
// Additional fields
|
||||
@@ -163,11 +164,12 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
|
||||
// The method returns a unique subscriptionID that can be used in Unsubscribe.
|
||||
//
|
||||
// Example:
|
||||
// c := client.New(client.NewConfig())
|
||||
// subscriptionID := c.Subscribe("mytopic")
|
||||
// for m := range c.Messages {
|
||||
// fmt.Printf("New message: %s", m.Message)
|
||||
// }
|
||||
//
|
||||
// c := client.New(client.NewConfig())
|
||||
// subscriptionID := c.Subscribe("mytopic")
|
||||
// for m := range c.Messages {
|
||||
// fmt.Printf("New message: %s", m.Message)
|
||||
// }
|
||||
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -5,6 +5,16 @@
|
||||
#
|
||||
# default-host: https://ntfy.sh
|
||||
|
||||
# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
|
||||
# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
|
||||
# For an empty password, use empty double-quotes ("")
|
||||
#
|
||||
# default-user:
|
||||
# default-password:
|
||||
|
||||
# Default command will execute after "ntfy subscribe" receives a message if no command is provided in subscription below
|
||||
# default-command:
|
||||
|
||||
# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
|
||||
# or if you cann "ntfy subscribe --from-config" directly.
|
||||
#
|
||||
|
||||
@@ -12,11 +12,14 @@ const (
|
||||
|
||||
// Config is the config struct for a Client
|
||||
type Config struct {
|
||||
DefaultHost string `yaml:"default-host"`
|
||||
Subscribe []struct {
|
||||
DefaultHost string `yaml:"default-host"`
|
||||
DefaultUser string `yaml:"default-user"`
|
||||
DefaultPassword *string `yaml:"default-password"`
|
||||
DefaultCommand string `yaml:"default-command"`
|
||||
Subscribe []struct {
|
||||
Topic string `yaml:"topic"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Password *string `yaml:"password"`
|
||||
Command string `yaml:"command"`
|
||||
If map[string]string `yaml:"if"`
|
||||
} `yaml:"subscribe"`
|
||||
@@ -25,8 +28,11 @@ type Config struct {
|
||||
// NewConfig creates a new Config struct for a Client
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
DefaultHost: DefaultBaseURL,
|
||||
Subscribe: nil,
|
||||
DefaultHost: DefaultBaseURL,
|
||||
DefaultUser: "",
|
||||
DefaultPassword: nil,
|
||||
DefaultCommand: "",
|
||||
Subscribe: nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ func TestConfig_Load(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||
default-host: http://localhost
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
default-command: 'echo "Got the message: $message"'
|
||||
subscribe:
|
||||
- topic: no-command-with-auth
|
||||
user: phil
|
||||
@@ -22,19 +25,94 @@ subscribe:
|
||||
command: notify-send -i /usr/share/ntfy/logo.png "Important" "$m"
|
||||
if:
|
||||
priority: high,urgent
|
||||
- topic: defaults
|
||||
`), 0600))
|
||||
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, 3, len(conf.Subscribe))
|
||||
require.Equal(t, "philipp", conf.DefaultUser)
|
||||
require.Equal(t, "mypass", *conf.DefaultPassword)
|
||||
require.Equal(t, `echo "Got the message: $message"`, conf.DefaultCommand)
|
||||
require.Equal(t, 4, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "mypass", conf.Subscribe[0].Password)
|
||||
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
||||
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
||||
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
||||
require.Equal(t, "alerts", conf.Subscribe[2].Topic)
|
||||
require.Equal(t, `notify-send -i /usr/share/ntfy/logo.png "Important" "$m"`, conf.Subscribe[2].Command)
|
||||
require.Equal(t, "high,urgent", conf.Subscribe[2].If["priority"])
|
||||
require.Equal(t, "defaults", conf.Subscribe[3].Topic)
|
||||
}
|
||||
|
||||
func TestConfig_EmptyPassword(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||
default-host: http://localhost
|
||||
default-user: philipp
|
||||
default-password: ""
|
||||
subscribe:
|
||||
- topic: no-command-with-auth
|
||||
user: phil
|
||||
password: ""
|
||||
`), 0600))
|
||||
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, "philipp", conf.DefaultUser)
|
||||
require.Equal(t, "", *conf.DefaultPassword)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Equal(t, "", *conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
func TestConfig_NullPassword(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||
default-host: http://localhost
|
||||
default-user: philipp
|
||||
default-password: ~
|
||||
subscribe:
|
||||
- topic: no-command-with-auth
|
||||
user: phil
|
||||
password: ~
|
||||
`), 0600))
|
||||
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, "philipp", conf.DefaultUser)
|
||||
require.Nil(t, conf.DefaultPassword)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
func TestConfig_NoPassword(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||
default-host: http://localhost
|
||||
default-user: philipp
|
||||
subscribe:
|
||||
- topic: no-command-with-auth
|
||||
user: phil
|
||||
`), 0600))
|
||||
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, "philipp", conf.DefaultUser)
|
||||
require.Nil(t, conf.DefaultPassword)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ func WithClick(url string) PublishOption {
|
||||
return WithHeader("X-Click", url)
|
||||
}
|
||||
|
||||
// WithIcon makes the notification use the given URL as its icon
|
||||
func WithIcon(icon string) PublishOption {
|
||||
return WithHeader("X-Icon", icon)
|
||||
}
|
||||
|
||||
// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
|
||||
// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
|
||||
func WithActions(value string) PublishOption {
|
||||
|
||||
@@ -97,11 +97,11 @@ func execUserAccess(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func changeAccess(c *cli.Context, manager auth.Manager, username string, topic string, perms string) error {
|
||||
if !util.InStringList([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
|
||||
if !util.Contains([]string{"", "read-write", "rw", "read-only", "read", "ro", "write-only", "write", "wo", "none", "deny"}, perms) {
|
||||
return errors.New("permission must be one of: read-write, read-only, write-only, or deny (or the aliases: read, ro, write, wo, none)")
|
||||
}
|
||||
read := util.InStringList([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
|
||||
write := util.InStringList([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
|
||||
read := util.Contains([]string{"read-write", "rw", "read-only", "read", "ro"}, perms)
|
||||
write := util.Contains([]string{"read-write", "rw", "write-only", "write", "wo"}, perms)
|
||||
user, err := manager.User(username)
|
||||
if err == auth.ErrNotFound {
|
||||
return fmt.Errorf("user %s does not exist", username)
|
||||
|
||||
@@ -40,7 +40,7 @@ func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli
|
||||
// This function also maps aliases, so a .yml file can contain short options, or options with underscores
|
||||
// instead of dashes. See https://github.com/binwiederhier/ntfy/issues/255.
|
||||
func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputSourceContext, error) {
|
||||
var rawConfig map[interface{}]interface{}
|
||||
var rawConfig map[any]any
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -28,17 +28,18 @@ var flagsPublish = append(
|
||||
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
|
||||
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
|
||||
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
|
||||
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
||||
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
||||
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
||||
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||
&cli.IntFlag{Name: "wait-pid", Aliases: []string{"pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"},
|
||||
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
|
||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
||||
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
|
||||
&cli.IntFlag{Name: "wait-pid", Aliases: []string{"wait_pid", "pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"},
|
||||
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
|
||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"no_firebase", "F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
||||
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"env_topic", "P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
|
||||
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"},
|
||||
)
|
||||
|
||||
@@ -48,7 +49,7 @@ var cmdPublish = &cli.Command{
|
||||
Usage: "Send message via a ntfy server",
|
||||
UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
|
||||
ntfy publish [OPTIONS..] --wait-cmd COMMAND...
|
||||
NTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE...]`,
|
||||
NTFY_TOPIC=.. ntfy publish [OPTIONS..] [MESSAGE...]`,
|
||||
Action: execPublish,
|
||||
Category: categoryClient,
|
||||
Flags: flagsPublish,
|
||||
@@ -64,13 +65,14 @@ Examples:
|
||||
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
|
||||
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
|
||||
ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked
|
||||
ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon
|
||||
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
|
||||
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
|
||||
ntfy pub -u phil:mypass secret Psst # Publish with username/password
|
||||
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_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
|
||||
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
||||
|
||||
@@ -90,6 +92,7 @@ func execPublish(c *cli.Context) error {
|
||||
tags := c.String("tags")
|
||||
delay := c.String("delay")
|
||||
click := c.String("click")
|
||||
icon := c.String("icon")
|
||||
actions := c.String("actions")
|
||||
attach := c.String("attach")
|
||||
filename := c.String("filename")
|
||||
@@ -120,6 +123,9 @@ func execPublish(c *cli.Context) error {
|
||||
if click != "" {
|
||||
options = append(options, client.WithClick(click))
|
||||
}
|
||||
if icon != "" {
|
||||
options = append(options, client.WithIcon(icon))
|
||||
}
|
||||
if actions != "" {
|
||||
options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
|
||||
}
|
||||
@@ -154,6 +160,8 @@ func execPublish(c *cli.Context) error {
|
||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||
}
|
||||
options = append(options, client.WithBasicAuth(user, pass))
|
||||
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
||||
}
|
||||
if pid > 0 {
|
||||
newMessage, err := waitForProcess(pid)
|
||||
@@ -206,10 +214,11 @@ func execPublish(c *cli.Context) error {
|
||||
// parseTopicMessageCommand reads the topic and the remaining arguments from the context.
|
||||
|
||||
// There are a few cases to consider:
|
||||
// ntfy publish <topic> [<message>]
|
||||
// ntfy publish --wait-cmd <topic> <command>
|
||||
// NTFY_TOPIC=.. ntfy publish [<message>]
|
||||
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
|
||||
//
|
||||
// ntfy publish <topic> [<message>]
|
||||
// ntfy publish --wait-cmd <topic> <command>
|
||||
// NTFY_TOPIC=.. ntfy publish [<message>]
|
||||
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
|
||||
func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) {
|
||||
var args []string
|
||||
topic, args, err = parseTopicAndArgs(c)
|
||||
@@ -232,13 +241,9 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com
|
||||
}
|
||||
|
||||
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
|
||||
envTopic := c.Bool("env-topic")
|
||||
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 = os.Getenv("NTFY_TOPIC")
|
||||
if topic == "" {
|
||||
return "", nil, errors.New("when --env-topic is passed, must define NTFY_TOPIC environment variable")
|
||||
}
|
||||
envTopic := os.Getenv("NTFY_TOPIC")
|
||||
if envTopic != "" {
|
||||
topic = envTopic
|
||||
return topic, remainingArgs(c, 0), nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import "syscall"
|
||||
|
||||
func processExists(pid int) bool {
|
||||
err := syscall.Kill(pid, syscall.Signal(0))
|
||||
return err == nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||
|
||||
app, _, _, _ := newTestApp()
|
||||
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()
|
||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
||||
@@ -52,6 +53,7 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
|
||||
"--tags", "tag1,tag2",
|
||||
// No --delay, --email
|
||||
"--click", "https://ntfy.sh",
|
||||
"--icon", "https://ntfy.sh/static/img/ntfy.png",
|
||||
"--attach", "https://f-droid.org/F-Droid.apk",
|
||||
"--filename", "fdroid.apk",
|
||||
"--no-cache",
|
||||
@@ -73,6 +75,7 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
|
||||
require.Equal(t, "", m.Attachment.Owner)
|
||||
require.Equal(t, int64(0), m.Attachment.Expires)
|
||||
require.Equal(t, "", m.Attachment.Type)
|
||||
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build darwin || linux || dragonfly || freebsd || netbsd || openbsd
|
||||
// +build darwin linux dragonfly freebsd netbsd openbsd
|
||||
|
||||
package cmd
|
||||
|
||||
import "syscall"
|
||||
59
cmd/serve.go
59
cmd/serve.go
@@ -5,15 +5,18 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/log"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/server"
|
||||
@@ -35,11 +38,14 @@ var flagsServe = append(
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
|
||||
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.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: "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"}),
|
||||
@@ -99,12 +105,15 @@ func execServe(c *cli.Context) error {
|
||||
listenHTTP := c.String("listen-http")
|
||||
listenHTTPS := c.String("listen-https")
|
||||
listenUnix := c.String("listen-unix")
|
||||
listenUnixMode := c.Int("listen-unix-mode")
|
||||
keyFile := c.String("key-file")
|
||||
certFile := c.String("cert-file")
|
||||
firebaseKeyFile := c.String("firebase-key-file")
|
||||
cacheFile := c.String("cache-file")
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
cacheStartupQueries := c.String("cache-startup-queries")
|
||||
cacheBatchSize := c.Int("cache-batch-size")
|
||||
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||
authFile := c.String("auth-file")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||
@@ -154,14 +163,18 @@ func execServe(c *cli.Context) error {
|
||||
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
||||
} else if attachmentCacheDir != "" && baseURL == "" {
|
||||
return errors.New("if attachment-cache-dir is set, base-url must also be set")
|
||||
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") && strings.HasSuffix(baseURL, "/") {
|
||||
return errors.New("if set, base-url must start with http:// or https://, and must not end with a slash (/)")
|
||||
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
||||
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||
return errors.New("if set, base-url must start with http:// or https://")
|
||||
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
||||
return errors.New("if set, base-url must not end with a slash (/)")
|
||||
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
||||
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
||||
} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
|
||||
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
|
||||
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
||||
return errors.New("if set, upstream-base-url must not end with a slash (/)")
|
||||
} else if upstreamBaseURL != "" && baseURL == "" {
|
||||
return errors.New("if upstream-base-url is set, base-url must also be set")
|
||||
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
|
||||
@@ -201,16 +214,14 @@ func execServe(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// Resolve hosts
|
||||
visitorRequestLimitExemptIPs := make([]string, 0)
|
||||
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
|
||||
for _, host := range visitorRequestLimitExemptHosts {
|
||||
ips, err := net.LookupIP(host)
|
||||
ips, err := parseIPHostPrefix(host)
|
||||
if err != nil {
|
||||
log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
|
||||
continue
|
||||
}
|
||||
for _, ip := range ips {
|
||||
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ip.String())
|
||||
}
|
||||
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ips...)
|
||||
}
|
||||
|
||||
// Run server
|
||||
@@ -219,12 +230,15 @@ func execServe(c *cli.Context) error {
|
||||
conf.ListenHTTP = listenHTTP
|
||||
conf.ListenHTTPS = listenHTTPS
|
||||
conf.ListenUnix = listenUnix
|
||||
conf.ListenUnixMode = fs.FileMode(listenUnixMode)
|
||||
conf.KeyFile = keyFile
|
||||
conf.CertFile = certFile
|
||||
conf.FirebaseKeyFile = firebaseKeyFile
|
||||
conf.CacheFile = cacheFile
|
||||
conf.CacheDuration = cacheDuration
|
||||
conf.CacheStartupQueries = cacheStartupQueries
|
||||
conf.CacheBatchSize = cacheBatchSize
|
||||
conf.CacheBatchTimeout = cacheBatchTimeout
|
||||
conf.AuthFile = authFile
|
||||
conf.AuthDefaultRead = authDefaultRead
|
||||
conf.AuthDefaultWrite = authDefaultWrite
|
||||
@@ -295,6 +309,31 @@ func sigHandlerConfigReload(config string) {
|
||||
}
|
||||
}
|
||||
|
||||
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
||||
// Try parsing as prefix, e.g. 10.0.1.0/24
|
||||
prefix, err := netip.ParsePrefix(host)
|
||||
if err == nil {
|
||||
prefixes = append(prefixes, prefix.Masked())
|
||||
return prefixes, nil
|
||||
}
|
||||
// Not a prefix, parse as host or IP (LookupHost passes through an IP as is)
|
||||
ips, err := net.LookupHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ipStr := range ips {
|
||||
ip, err := netip.ParseAddr(ipStr)
|
||||
if err == nil {
|
||||
prefix, err := ip.Prefix(ip.BitLen())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s successfully parsed but unable to make prefix: %s", ip.String(), err.Error())
|
||||
}
|
||||
prefixes = append(prefixes, prefix.Masked())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) {
|
||||
newLevelStr, err := inputSource.String("log-level")
|
||||
if err != nil {
|
||||
|
||||
@@ -2,17 +2,19 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -70,6 +72,22 @@ func TestCLI_Serve_WebSocket(t *testing.T) {
|
||||
require.Equal(t, "mytopic", m.Topic)
|
||||
}
|
||||
|
||||
func TestIP_Host_Parsing(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"1.1.1.1": "1.1.1.1/32",
|
||||
"fd00::1234": "fd00::1234/128",
|
||||
"192.168.0.3/24": "192.168.0.0/24",
|
||||
"10.1.2.3/8": "10.0.0.0/8",
|
||||
"201:be93::4a6/21": "201:b800::/21",
|
||||
}
|
||||
for q, expectedAnswer := range cases {
|
||||
ips, err := parseIPHostPrefix(q)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 1, len(ips))
|
||||
assert.Equal(t, expectedAnswer, ips[0].String())
|
||||
}
|
||||
}
|
||||
|
||||
func newEmptyFile(t *testing.T) string {
|
||||
filename := filepath.Join(t.TempDir(), "empty")
|
||||
require.Nil(t, os.WriteFile(filename, []byte{}, 0600))
|
||||
|
||||
@@ -29,8 +29,8 @@ var flagsSubscribe = append(
|
||||
flagsDefault,
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
||||
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
|
||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"},
|
||||
&cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
|
||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
|
||||
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
|
||||
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
|
||||
)
|
||||
@@ -175,11 +175,29 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
for filter, value := range s.If {
|
||||
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
||||
}
|
||||
if s.User != "" && s.Password != "" {
|
||||
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
|
||||
var user string
|
||||
var password *string
|
||||
if s.User != "" {
|
||||
user = s.User
|
||||
} else if conf.DefaultUser != "" {
|
||||
user = conf.DefaultUser
|
||||
}
|
||||
if s.Password != nil {
|
||||
password = s.Password
|
||||
} else if conf.DefaultPassword != nil {
|
||||
password = conf.DefaultPassword
|
||||
}
|
||||
if user != "" && password != nil {
|
||||
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
|
||||
}
|
||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||
cmds[subscriptionID] = s.Command
|
||||
if s.Command != "" {
|
||||
cmds[subscriptionID] = s.Command
|
||||
} else if conf.DefaultCommand != "" {
|
||||
cmds[subscriptionID] = conf.DefaultCommand
|
||||
} else {
|
||||
cmds[subscriptionID] = ""
|
||||
}
|
||||
}
|
||||
if topic != "" {
|
||||
subscriptionID := cl.Subscribe(topic, options...)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||
// +build linux dragonfly freebsd netbsd openbsd
|
||||
|
||||
package cmd
|
||||
|
||||
const (
|
||||
@@ -21,9 +21,9 @@ func init() {
|
||||
|
||||
var flagsUser = append(
|
||||
flagsDefault,
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||
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"}),
|
||||
)
|
||||
|
||||
var cmdUser = &cli.Command{
|
||||
@@ -273,7 +273,7 @@ func createAuthManager(c *cli.Context) (auth.Manager, error) {
|
||||
return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
|
||||
} else if !util.FileExists(authFile) {
|
||||
return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
|
||||
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
||||
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
||||
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
|
||||
}
|
||||
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
|
||||
|
||||
137
docs/config.md
137
docs/config.md
@@ -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
|
||||
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.
|
||||
@@ -441,8 +460,15 @@ by forwarding the `Connection` and `Upgrade` headers accordingly.
|
||||
In this example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
|
||||
or the root domain:
|
||||
|
||||
=== "nginx (/etc/nginx/sites-*/ntfy)"
|
||||
=== "nginx (convenient)"
|
||||
```
|
||||
# /etc/nginx/sites-*/ntfy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# This is how ntfy.sh is configured.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ntfy.sh;
|
||||
@@ -486,7 +512,7 @@ or the root domain:
|
||||
server_name ntfy.sh;
|
||||
|
||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
@@ -515,8 +541,70 @@ or the root domain:
|
||||
}
|
||||
```
|
||||
|
||||
=== "Apache2 (/etc/apache2/sites-*/ntfy.conf)"
|
||||
=== "nginx (more secure)"
|
||||
```
|
||||
# /etc/nginx/sites-*/ntfy
|
||||
#
|
||||
# This config requires the use of the -L flag in curl to redirect to HTTPS, and it keeps nginx output buffering
|
||||
# enabled. While recommended, I have had issues with that in the past.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ntfy.sh;
|
||||
|
||||
location / {
|
||||
return 302 https://$http_host$request_uri$is_args$query_string;
|
||||
|
||||
proxy_pass http://127.0.0.1:2586;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_connect_timeout 3m;
|
||||
proxy_send_timeout 3m;
|
||||
proxy_read_timeout 3m;
|
||||
|
||||
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name ntfy.sh;
|
||||
|
||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:2586;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_connect_timeout 3m;
|
||||
proxy_send_timeout 3m;
|
||||
proxy_read_timeout 3m;
|
||||
|
||||
client_max_body_size 20m; # Must be >= attachment-file-size-limit in /etc/ntfy/server.yml
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Apache2"
|
||||
```
|
||||
# /etc/apache2/sites-*/ntfy.conf
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName ntfy.sh
|
||||
|
||||
@@ -655,6 +743,10 @@ curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c
|
||||
{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"}
|
||||
```
|
||||
|
||||
Note that the self-hosted server literally sends the message `New message` for every message, even if your message
|
||||
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
|
||||
it'll show `New message` as a popup.
|
||||
|
||||
## Rate limiting
|
||||
!!! info
|
||||
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
||||
@@ -733,19 +825,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:
|
||||
|
||||
### 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
|
||||
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.
|
||||
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:
|
||||
|
||||
``` yaml
|
||||
cache-batch-size: 25
|
||||
cache-batch-timeout: "1s"
|
||||
cache-startup-queries: |
|
||||
pragma journal_mode = WAL;
|
||||
pragma synchronous = normal;
|
||||
pragma temp_store = memory;
|
||||
pragma busy_timeout = 15000;
|
||||
vacuum;
|
||||
```
|
||||
|
||||
### For systemd services
|
||||
@@ -805,8 +905,24 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
|
||||
=== "/etc/nginx/nginx.conf"
|
||||
```
|
||||
# Rate limit all IP addresses
|
||||
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
|
||||
http {
|
||||
geo $limited {
|
||||
default 1;
|
||||
116.203.112.46/32 0;
|
||||
132.226.42.65/32 0;
|
||||
...
|
||||
}
|
||||
map $limited $limitkey {
|
||||
1 $binary_remote_addr;
|
||||
0 "";
|
||||
}
|
||||
limit_req_zone $limitkey zone=one:10m rate=45r/m;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -835,7 +951,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
|
||||
logpath = /var/log/nginx/error.log
|
||||
findtime = 600
|
||||
bantime = 7200
|
||||
bantime = 14400
|
||||
maxretry = 10
|
||||
```
|
||||
|
||||
@@ -875,12 +991,15 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
|
||||
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
|
||||
| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on |
|
||||
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
|
||||
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
|
||||
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
|
||||
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
|
||||
| `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-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-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. |
|
||||
@@ -894,9 +1013,9 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
|
||||
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
|
||||
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
|
||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
|
||||
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
||||
| `manager-interval` | `NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
||||
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
||||
| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers |
|
||||
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
|
||||
@@ -945,6 +1064,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]
|
||||
--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-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]
|
||||
--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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Active deprecations
|
||||
_No active deprecations_
|
||||
|
||||
## Previous deprecations
|
||||
|
||||
### 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.
|
||||
|
||||
=== "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"
|
||||
```
|
||||
|
||||
## Previous deprecations
|
||||
|
||||
### <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)
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ Build related:
|
||||
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).
|
||||
|
||||
### 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
|
||||
|
||||
* [Go](https://go.dev/) (required for main server)
|
||||
@@ -58,8 +65,8 @@ These steps **assume Ubuntu**. Steps may vary on different Linux distributions.
|
||||
|
||||
First, install [Go](https://go.dev/) (see [official instructions](https://go.dev/doc/install)):
|
||||
``` shell
|
||||
wget https://go.dev/dl/go1.18.linux-amd64.tar.gz
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
|
||||
wget https://go.dev/dl/go1.19.1.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.19.1.linux-amd64.tar.gz
|
||||
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
|
||||
go version # verifies that it worked
|
||||
```
|
||||
@@ -72,7 +79,7 @@ goreleaser -v # verifies that it worked
|
||||
|
||||
Install [nodejs](https://nodejs.org/en/) (see [official instructions](https://nodejs.org/en/download/package-manager/)):
|
||||
``` shell
|
||||
curl -fsSL https://deb.nodesource.com/setup_17.x | sudo -E bash -
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
npm -v # verifies that it worked
|
||||
```
|
||||
|
||||
113
docs/examples.md
113
docs/examples.md
@@ -101,7 +101,7 @@ It looked something like this:
|
||||
You can easily integrate ntfy into Ansible, Salt, or Puppet to notify you when runs are done or are highstated.
|
||||
One of my co-workers uses the following Ansible task to let him know when things are done:
|
||||
|
||||
```yml
|
||||
``` yaml
|
||||
- name: Send ntfy.sh update
|
||||
uri:
|
||||
url: "https://ntfy.sh/{{ ntfy_channel }}"
|
||||
@@ -109,12 +109,38 @@ One of my co-workers uses the following Ansible task to let him know when things
|
||||
body: "{{ inventory_hostname }} reseeding complete"
|
||||
```
|
||||
|
||||
There's also a dedicated Ansible action plugin (one which runs on the Ansible controller) called
|
||||
[ansible-ntfy](https://github.com/jpmens/ansible-ntfy). The following task posts a message
|
||||
to ntfy at its default URL (`attrs` and other attributes are optional):
|
||||
|
||||
``` yaml
|
||||
- name: "Notify ntfy that we're done"
|
||||
ntfy:
|
||||
msg: "deployment on {{ inventory_hostname }} is complete. 🐄"
|
||||
attrs:
|
||||
tags: [ heavy_check_mark ]
|
||||
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)
|
||||
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.
|
||||
|
||||
Example docker-compose.yml:
|
||||
```yml
|
||||
``` yaml
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
@@ -342,9 +368,22 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
|
||||

|
||||
|
||||
## Gatus
|
||||
To use ntfy with [Gatus](https://github.com/TwiN/gatus), you can use the `ntfy` alerting provider like so:
|
||||
|
||||
An example for a custom alert with [Gatus](https://github.com/TwiN/gatus):
|
||||
``` yaml
|
||||
```yaml
|
||||
alerting:
|
||||
ntfy:
|
||||
url: "https://ntfy.sh"
|
||||
topic: "YOUR_NTFY_TOPIC"
|
||||
priority: 3
|
||||
```
|
||||
|
||||
For more information on using ntfy with Gatus, refer to [Configuring ntfy alerts](https://github.com/TwiN/gatus#configuring-ntfy-alerts).
|
||||
|
||||
<details>
|
||||
<summary>Alternative: Using the custom alerting provider</summary>
|
||||
|
||||
```yaml
|
||||
alerting:
|
||||
custom:
|
||||
url: "https://ntfy.sh"
|
||||
@@ -369,6 +408,9 @@ alerting:
|
||||
RESOLVED: "white_check_mark"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Jellyseerr/Overseerr 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.
|
||||
@@ -449,6 +491,43 @@ You can now test the notifications and apply them to monitors:
|
||||
<a href="../static/img/uptimekuma-ios-up.jpg"><img src="../static/img/uptimekuma-ios-up.jpg"/></a>
|
||||
</div>
|
||||
|
||||
## UptimeRobot
|
||||
Go to your [UptimeRobot](https://github.com/uptimerobot) My Settings > Alert Contacts > Add Alert Contact
|
||||
Select **Alert Contact Type** = Webhook. Then set your desired **Friendly Name** (e.g. "ntfy-sh-UP"), **URL to Notify**, **POST value** and select checkbox **Send as JSON (application/json)**. Make sure to send the JSON POST request to ntfy.domain.com without the topic name in the url and include the "topic" name in the JSON body.
|
||||
|
||||
<div id="uptimerobot-monitor-setup" class="screenshots">
|
||||
<a href="../static/img/uptimerobot-setup.jpg"><img src="../static/img/uptimerobot-setup.jpg"/></a>
|
||||
</div>
|
||||
|
||||
``` json
|
||||
{
|
||||
"topic":"myTopic",
|
||||
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
|
||||
"message": "*alertDetails*",
|
||||
"tags": ["green_circle"],
|
||||
"priority": 3,
|
||||
"click": https://uptimerobot.com/dashboard#*monitorID*
|
||||
}
|
||||
```
|
||||
You can create two Alert Contacts each with a different icon and priority, for example:
|
||||
|
||||
``` json
|
||||
{
|
||||
"topic":"myTopic",
|
||||
"title": "*monitorFriendlyName* *alertTypeFriendlyName*",
|
||||
"message": "*alertDetails*",
|
||||
"tags": ["red_circle"],
|
||||
"priority": 3,
|
||||
"click": https://uptimerobot.com/dashboard#*monitorID*
|
||||
}
|
||||
```
|
||||
You can now add the created Alerts Contact(s) to the monitor(s) and test the notifications:
|
||||
|
||||
<div id="uptimerobot-monitor-screenshots" class="screenshots">
|
||||
<a href="../static/img/uptimerobot-test.jpg"><img src="../static/img/uptimerobot-test.jpg"/></a>
|
||||
</div>
|
||||
|
||||
|
||||
## Apprise
|
||||
ntfy is integrated natively into [Apprise](https://github.com/caronc/apprise) (also check out the
|
||||
[Apprise/ntfy wiki page](https://github.com/caronc/apprise/wiki/Notify_ntfy)).
|
||||
@@ -467,3 +546,29 @@ apprise -vv -t "Test Message Title" -b "Test Message Body" \
|
||||
ntfy://ntfy.example.com/mytopic
|
||||
```
|
||||
|
||||
|
||||
## Rundeck
|
||||
Rundeck by default sends only HTML email which is not processed by ntfy SMTP server. Append following configurations to
|
||||
[rundeck-config.properties](https://docs.rundeck.com/docs/administration/configuration/config-file-reference.html) :
|
||||
|
||||
```
|
||||
# Template
|
||||
rundeck.mail.template.file=/path/to/template.html
|
||||
rundeck.mail.template.log.formatted=false
|
||||
```
|
||||
|
||||
Example `template.html`:
|
||||
```html
|
||||
<div>Execution ${execution.id} was <b>${execution.status}</b></div>
|
||||
<ul>
|
||||
<li><a href="${execution.href}">Execution result</a></li>
|
||||
<li><a href="${job.href}">Job</a></li>
|
||||
<li><a href="${execution.projectHref}">Project: ${execution.project}</a></li>
|
||||
<li><a href="${rundeck.href}">Rundeck</a></li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Add notification on Rundeck (attachment type must be: `Attached as file to email`):
|
||||

|
||||
|
||||
|
||||
|
||||
44
docs/faq.md
44
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.
|
||||
|
||||
## 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
|
||||
the service.
|
||||
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
|
||||
server without signup and free of charge, I may also offer paid plans in the future.
|
||||
|
||||
## 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?
|
||||
As per usual with pub-sub, all subscribers receive notifications if they are subscribed to a topic.
|
||||
@@ -23,7 +32,7 @@ to facilitate service restarts, message polling and to overcome client network d
|
||||
Yes. The server (including this Web UI) can be self-hosted, and the Android/iOS app supports adding topics from
|
||||
your own server as well. Check out the [install instructions](install.md).
|
||||
|
||||
## Why is Firebase used?
|
||||
## Is Firebase used?
|
||||
In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
|
||||
published to Firebase Cloud Messaging (FCM) (if `FirebaseKeyFile` is set, which it is on ntfy.sh). This
|
||||
is to facilitate notifications on Android.
|
||||
@@ -43,9 +52,26 @@ decent now.
|
||||
server and listens for incoming notifications. This consumes additional battery (see above),
|
||||
but delivers notifications instantly.
|
||||
|
||||
## Where can I donate?
|
||||
Many people have asked (thanks for that!), but I am currently not accepting any donations. The cost is manageable
|
||||
($25/month for hosting, and $99/year for the Apple cert) right now, and I don't want to have to feel obligated to
|
||||
anyone by accepting their money.
|
||||
## 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 may ask for donations in the future, though. After all, $400 per year isn't nothing...
|
||||
## 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?
|
||||
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
|
||||
security risk. So technically, it does not need to be disabled.
|
||||
|
||||
However, if you still want to disable it, you can do so with the `web-root: disable` option in the `server.yml` file.
|
||||
|
||||
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
||||
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
||||
|
||||
## Where can I donate?
|
||||
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
|
||||
appreciated.
|
||||
|
||||
383
docs/install.md
383
docs/install.md
@@ -17,7 +17,7 @@ We support amd64, armv7 and arm64.
|
||||
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))
|
||||
|
||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI][subscribe/cli.md]
|
||||
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
||||
for details).
|
||||
|
||||
## Linux binaries
|
||||
@@ -26,37 +26,37 @@ deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_1.27.2_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_1.27.2_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_1.30.1_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_1.30.1_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.30.1_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_1.27.2_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_1.27.2_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_1.30.1_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_1.30.1_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.30.1_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_1.27.2_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_1.27.2_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_1.30.1_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_1.30.1_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.30.1_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_1.27.2_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_1.27.2_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.27.2_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_1.30.1_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_1.30.1_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.30.1_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
@@ -65,9 +65,10 @@ Installation via Debian repository:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' \
|
||||
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
@@ -77,10 +78,11 @@ Installation via Debian repository:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=armhf] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
sudo systemctl enable ntfy
|
||||
@@ -89,10 +91,11 @@ Installation via Debian repository:
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
|
||||
sudo apt install apt-transport-https
|
||||
sudo sh -c "echo 'deb [arch=arm64] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
|
||||
> /etc/apt/sources.list.d/archive.heckel.io.list"
|
||||
sudo apt update
|
||||
sudo apt install ntfy
|
||||
sudo systemctl enable ntfy
|
||||
@@ -103,7 +106,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -111,7 +114,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -119,7 +122,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -127,7 +130,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -137,28 +140,28 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_amd64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv6.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_armv7.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
@@ -182,20 +185,22 @@ ntfy is packaged in nixpkgs as `ntfy-sh`. It can be installed by adding the pack
|
||||
nix-env -iA ntfy-sh
|
||||
```
|
||||
|
||||
NixOS also supports [declarative setup of the ntfy server](https://search.nixos.org/options?channel=unstable&show=services.ntfy-sh.enable&from=0&size=50&sort=relevance&type=packages&query=ntfy).
|
||||
|
||||
## macOS
|
||||
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.27.2/ntfy_1.27.2_macOS_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_macOS_all.tar.gz),
|
||||
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
|
||||
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.27.2/ntfy_1.27.2_macOS_all.tar.gz > ntfy_1.27.2_macOS_all.tar.gz
|
||||
tar zxvf ntfy_1.27.2_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_1.27.2_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_macOS_all.tar.gz > ntfy_1.30.1_macOS_all.tar.gz
|
||||
tar zxvf ntfy_1.30.1_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_1.30.1_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_1.27.2_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_1.30.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
@@ -207,7 +212,7 @@ ntfy --help
|
||||
|
||||
## Windows
|
||||
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.27.2/ntfy_1.27.2_windows_x86_64.zip),
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.30.1/ntfy_1.30.1_windows_x86_64.zip),
|
||||
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).
|
||||
@@ -228,6 +233,11 @@ The server exposes its web UI and the API on port 80, so you need to expose that
|
||||
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,
|
||||
you should map `/etc/ntfy`, so you can edit `/etc/ntfy/server.yml`.
|
||||
|
||||
!!! info
|
||||
Note that the Docker image **does not contain a `/etc/ntfy/server.yml` file**. If you'd like to use a config file,
|
||||
please manually create one outside the image and map it as a volume, e.g. via `-v /etc/ntfy:/etc/ntfy`. You may
|
||||
use the [`server.yml` file on GitHub](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml) as a template.
|
||||
|
||||
Basic usage (no cache or additional config):
|
||||
```
|
||||
docker run -p 80:80 -it binwiederhier/ntfy serve
|
||||
@@ -286,3 +296,298 @@ COPY server.yml /etc/ntfy/server.yml
|
||||
ENTRYPOINT ["ntfy", "serve"]
|
||||
```
|
||||
This image can be pushed to a container registry and shipped independently. All that's needed when running it is mapping ntfy's port to a host port.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
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 persistent cache, and a standalone
|
||||
unmanned pod.
|
||||
|
||||
|
||||
=== "deployment"
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: ntfy
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ntfy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ntfy
|
||||
spec:
|
||||
containers:
|
||||
- name: ntfy
|
||||
image: binwiederhier/ntfy
|
||||
args: ["serve"]
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: "/etc/ntfy"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: ntfy
|
||||
---
|
||||
# Basic service for port 80
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ntfy
|
||||
spec:
|
||||
selector:
|
||||
app: ntfy
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
=== "stateful set"
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: ntfy
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: ntfy
|
||||
serviceName: ntfy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: ntfy
|
||||
spec:
|
||||
containers:
|
||||
- name: ntfy
|
||||
image: binwiederhier/ntfy
|
||||
args: ["serve", "--cache-file /var/cache/ntfy/cache.db"]
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: "/etc/ntfy"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: ntfy
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: cache
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
```
|
||||
|
||||
=== "pod"
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
app: ntfy
|
||||
spec:
|
||||
containers:
|
||||
- name: ntfy
|
||||
image: binwiederhier/ntfy
|
||||
args: ["serve"]
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: "/etc/ntfy"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: ntfy
|
||||
```
|
||||
|
||||
Configuration is relatively straightforward. As an example, a minimal configuration is provided.
|
||||
|
||||
=== "resource definition"
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ntfy
|
||||
data:
|
||||
server.yml: |
|
||||
# Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml
|
||||
base-url: https://ntfy.sh
|
||||
```
|
||||
|
||||
=== "from-file"
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
146
docs/integrations.md
Normal file
146
docs/integrations.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Integrations + community projects
|
||||
|
||||
There are quite a few projects that work with ntfy, integrate ntfy, or have been built around ntfy. It's super exciting to see what you guys have come up with. Feel free to [create a pull request on GitHub](https://github.com/binwiederhier/ntfy/issues) to add your own project here.
|
||||
|
||||
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
|
||||
|
||||
## Public ntfy servers
|
||||
|
||||
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!**
|
||||
|
||||
| URL | Country |
|
||||
|---------------------------------------------------|--------------------|
|
||||
| [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
|
||||
|
||||
- [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
|
||||
- [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
|
||||
- [Radarr](https://radarr.video/) ⭐ - Movie collection manager for Usenet and BitTorrent users
|
||||
- [Sonarr](https://sonarr.tv/) ⭐ - PVR for Usenet and BitTorrent users
|
||||
- [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
|
||||
- [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
|
||||
|
||||
- [Element](https://f-droid.org/packages/im.vector.app/) ⭐ - Matrix client
|
||||
- [SchildiChat](https://schildi.chat/android/) ⭐ - Matrix client
|
||||
- [Tusky](https://tusky.app/) ⭐ - Fediverse client
|
||||
- [Fedilab](https://fedilab.app/) - Fediverse client
|
||||
- [FindMyDevice](https://gitlab.com/Nulide/findmydevice/) - Find your Device with an SMS or online with the help of FMDServer
|
||||
- [Tox Push Message App](https://github.com/zoff99/tox_push_msg_app) - Tox Push Message App
|
||||
|
||||
## Libraries
|
||||
|
||||
- [ntfy-php-library](https://github.com/VerifiedJoseph/ntfy-php-library) - PHP library for sending messages using a ntfy server (PHP)
|
||||
- [ntfy-notifier](https://github.com/DAcodedBEAT/ntfy-notifier) - Symfony Notifier integration for ntfy (PHP)
|
||||
- [ntfpy](https://github.com/Nevalicjus/ntfpy) - API Wrapper for ntfy.sh (Python)
|
||||
- [pyntfy](https://github.com/DP44/pyntfy) - A module for interacting with ntfy notifications (Python)
|
||||
- [vntfy](https://github.com/lmangani/vntfy) - Barebone V client for ntfy (V)
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
## CLIs + GUIs
|
||||
|
||||
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
|
||||
- [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
|
||||
- [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
|
||||
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
|
||||
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
|
||||
|
||||
## Projects + scripts
|
||||
|
||||
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
|
||||
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
|
||||
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
|
||||
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
|
||||
- [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs)
|
||||
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
|
||||
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
|
||||
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
|
||||
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
|
||||
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
|
||||
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (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/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [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
|
||||
- [huginn-global-entry-notif](https://github.com/kylezoa/huginn-global-entry-notif) - Checks CBP API for available appointments with Huginn (JSON)
|
||||
- [ntfyer](https://github.com/KikyTokamuro/ntfyer) - Sending various information to your ntfy topic by time (TypeScript)
|
||||
- [git-simple-notifier](https://github.com/plamenjm/git-simple-notifier) - Script running git-log, checking for new repositories (Shell)
|
||||
- [ntfy-to-slack](https://github.com/ozskywalker/ntfy-to-slack) - Tool to subscribe to a ntfy topic and send the messages to a Slack webhook (Go)
|
||||
- [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_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
||||
- [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
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
|
||||
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
|
||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/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
|
||||
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/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
|
||||
- [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - volkerkrause.eu - 11/2022
|
||||
- [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) ⭐ - tldr.tech - 11/2022
|
||||
- [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - news.ycombinator.com - 11/2022
|
||||
- [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - lunarok-domotique.com - 11/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
|
||||
- [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
|
||||
- [A nifty push notification system: ntfy](https://jpmens.net/2022/10/30/a-nifty-push-notification-system-ntfy/) - jpmens.net - 10/2022
|
||||
- [Alarmanlage der dritten Art (YouTube video)](https://www.youtube.com/watch?v=altb5QLHbaU&feature=youtu.be) - youtube.com - 10/2022
|
||||
- [Neue Services: Ntfy, TikTok und RustDesk](https://adminforge.de/tools/neue-services-ntfy-tiktok-und-rustdesk/) - adminforge.de - 9/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
|
||||
- [NAS Synology et notifications avec ntfy](https://www.cachem.fr/synology-notifications-ntfy/) - cachem.fr - 9/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
|
||||
- [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
|
||||
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - svrforum.com - 8/2022
|
||||
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - sometimesir.com - 6/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/) ⭐ - reddit.com - 6/2022
|
||||
- [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
|
||||
376
docs/publish.md
376
docs/publish.md
@@ -885,16 +885,22 @@ is the only required one:
|
||||
``` powershell
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
"topic"="powershell"
|
||||
"title"="Low disk space alert"
|
||||
"message"="Disk space is low at 5.1 GB"
|
||||
"priority"=4
|
||||
"attach"="https://filesrv.lan/space.jpg"
|
||||
"filename"="diskspace.jpg"
|
||||
"tags"=@("warning","cd")
|
||||
"click"= "https://homecamera.lan/xasds1h2xsSsa/"
|
||||
"actions"=@[@{ "action"="view", "label"="Admin panel", "url"="https://filesrv.lan/admin" }]
|
||||
} | ConvertTo-Json
|
||||
topic = "mytopic"
|
||||
title = "Low disk space alert"
|
||||
message = "Disk space is low at 5.1 GB"
|
||||
priority = 4
|
||||
attach = "https://filesrv.lan/space.jpg"
|
||||
filename = "diskspace.jpg"
|
||||
tags = @("warning", "cd")
|
||||
click = "https://homecamera.lan/xasds1h2xsSsa/"
|
||||
actions = @(
|
||||
@{
|
||||
action = "view"
|
||||
label = "Admin panel"
|
||||
url = "https://filesrv.lan/admin"
|
||||
}
|
||||
)
|
||||
} | ConvertTo-Json
|
||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
|
||||
```
|
||||
|
||||
@@ -1160,7 +1166,7 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
topic: "myhome",
|
||||
message": "You left the house. Turn down the A/C?",
|
||||
message: "You left the house. Turn down the A/C?",
|
||||
actions: [
|
||||
{
|
||||
action: "view",
|
||||
@@ -1210,20 +1216,20 @@ Alternatively, the same actions can be defined as **JSON array**, if the notific
|
||||
``` powershell
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
"topic"="myhome"
|
||||
"message"="You left the house. Turn down the A/C?"
|
||||
"actions"=@(
|
||||
topic = "myhome"
|
||||
message = "You left the house. Turn down the A/C?"
|
||||
actions = @(
|
||||
@{
|
||||
"action"="view"
|
||||
"label"="Open portal"
|
||||
"url"="https://home.nest.com/"
|
||||
"clear"=true
|
||||
action = "view"
|
||||
label = "Open portal"
|
||||
url = "https://home.nest.com/"
|
||||
clear = $true
|
||||
},
|
||||
@{
|
||||
"action"="http",
|
||||
"label"="Turn down"
|
||||
"url"="https://api.nest.com/"
|
||||
"body"="{\"temperature\": 65}"
|
||||
action = "http"
|
||||
label = "Turn down"
|
||||
url = "https://api.nest.com/"
|
||||
body = '{"temperature": 65}'
|
||||
}
|
||||
)
|
||||
} | ConvertTo-Json
|
||||
@@ -1310,7 +1316,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl \
|
||||
-d "Somebody retweetet your tweet." \
|
||||
-d "Somebody retweeted your tweet." \
|
||||
-H "Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
||||
ntfy.sh/myhome
|
||||
```
|
||||
@@ -1320,7 +1326,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
ntfy publish \
|
||||
--actions="view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392" \
|
||||
myhome \
|
||||
"Somebody retweetet your tweet."
|
||||
"Somebody retweeted your tweet."
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
@@ -1329,14 +1335,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
Host: ntfy.sh
|
||||
Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392
|
||||
|
||||
Somebody retweetet your tweet.
|
||||
Somebody retweeted your tweet.
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
fetch('https://ntfy.sh/myhome', {
|
||||
method: 'POST',
|
||||
body: 'Somebody retweetet your tweet.',
|
||||
body: 'Somebody retweeted your tweet.',
|
||||
headers: {
|
||||
'Actions': 'view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392'
|
||||
}
|
||||
@@ -1345,7 +1351,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
|
||||
=== "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")
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
@@ -1354,14 +1360,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
``` powershell
|
||||
$uri = "https://ntfy.sh/myhome"
|
||||
$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
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
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" })
|
||||
```
|
||||
|
||||
@@ -1373,7 +1379,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
'header' =>
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"Actions: view, Open Twitter, https://twitter.com/binwiederhier/status/1467633927951163392",
|
||||
'content' => 'Somebody retweetet your tweet.'
|
||||
'content' => 'Somebody retweeted your tweet.'
|
||||
]
|
||||
]));
|
||||
```
|
||||
@@ -1385,7 +1391,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
curl ntfy.sh \
|
||||
-d '{
|
||||
"topic": "myhome",
|
||||
"message": "Somebody retweetet your tweet.",
|
||||
"message": "Somebody retweeted your tweet.",
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
@@ -1407,7 +1413,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
}
|
||||
]' \
|
||||
myhome \
|
||||
"Somebody retweetet your tweet."
|
||||
"Somebody retweeted your tweet."
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
@@ -1417,7 +1423,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
|
||||
{
|
||||
"topic": "myhome",
|
||||
"message": "Somebody retweetet your tweet.",
|
||||
"message": "Somebody retweeted your tweet.",
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
@@ -1434,7 +1440,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
topic: "myhome",
|
||||
message": "Somebody retweetet your tweet.",
|
||||
message": "Somebody retweeted your tweet.",
|
||||
actions: [
|
||||
{
|
||||
action: "view",
|
||||
@@ -1453,7 +1459,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
|
||||
body := `{
|
||||
"topic": "myhome",
|
||||
"message": "Somebody retweetet your tweet.",
|
||||
"message": "Somebody retweeted your tweet.",
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
@@ -1470,9 +1476,9 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
``` powershell
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
"topic"="myhome"
|
||||
"message"="Somebody retweetet your tweet."
|
||||
"actions"=@(
|
||||
topic = "myhome"
|
||||
message = "Somebody retweeted your tweet."
|
||||
actions = @(
|
||||
@{
|
||||
"action"="view"
|
||||
"label"="Open Twitter"
|
||||
@@ -1488,7 +1494,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
requests.post("https://ntfy.sh/",
|
||||
data=json.dumps({
|
||||
"topic": "myhome",
|
||||
"message": "Somebody retweetet your tweet.",
|
||||
"message": "Somebody retweeted your tweet.",
|
||||
"actions": [
|
||||
{
|
||||
"action": "view",
|
||||
@@ -1508,7 +1514,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
'header' => "Content-Type: application/json",
|
||||
'content' => json_encode([
|
||||
"topic": "myhome",
|
||||
"message": "Somebody retweetet your tweet.",
|
||||
"message": "Somebody retweeted your tweet.",
|
||||
"actions": [
|
||||
[
|
||||
"action": "view",
|
||||
@@ -1725,21 +1731,24 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
# Powershell requires the 'Depth' argument to equal 3 here to expand 'Extras',
|
||||
# otherwise it will read System.Collections.Hashtable in the returned JSON
|
||||
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
"topic"="wifey"
|
||||
"message"="Your wife requested you send a picture of yourself."
|
||||
"actions"=@(
|
||||
topic = "wifey"
|
||||
message = "Your wife requested you send a picture of yourself."
|
||||
actions = @(
|
||||
@{
|
||||
"action"="broadcast"
|
||||
"label"="Take picture"
|
||||
"extras"=@{
|
||||
"cmd"="pic"
|
||||
"camera"="front"
|
||||
action = "broadcast"
|
||||
label = "Take picture"
|
||||
extras = @{
|
||||
cmd ="pic"
|
||||
camera = "front"
|
||||
}
|
||||
}
|
||||
)
|
||||
} | ConvertTo-Json
|
||||
} | ConvertTo-Json -Depth 3
|
||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
|
||||
```
|
||||
|
||||
@@ -1993,24 +2002,26 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
# Powershell requires the 'Depth' argument to equal 3 here to expand 'headers',
|
||||
# otherwise it will read System.Collections.Hashtable in the returned JSON
|
||||
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
"topic"="myhome"
|
||||
"message"="Garage door has been open for 15 minutes. Close it?"
|
||||
"actions"=@(
|
||||
topic = "myhome"
|
||||
message = "Garage door has been open for 15 minutes. Close it?"
|
||||
actions = @(
|
||||
@{
|
||||
"action"="http",
|
||||
"label"="Close door"
|
||||
"url"="https://api.mygarage.lan/"
|
||||
"method"="PUT"
|
||||
"headers"=@{
|
||||
"Authorization"="Bearer zAzsx1sk.."
|
||||
action = "http"
|
||||
label = "Close door"
|
||||
url = "https://api.mygarage.lan/"
|
||||
method = "PUT"
|
||||
headers = @{
|
||||
Authorization = "Bearer zAzsx1sk.."
|
||||
}
|
||||
"body"="{\"action\": \"close\"}"
|
||||
body = '{"action": "close"}'
|
||||
}
|
||||
}
|
||||
)
|
||||
} | ConvertTo-Json
|
||||
} | ConvertTo-Json -Depth 3
|
||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -ContentType "application/json" -UseBasicParsing
|
||||
```
|
||||
|
||||
@@ -2209,8 +2220,8 @@ Here's an example showing how to upload an image:
|
||||
Host: ntfy.sh
|
||||
Filename: flower.jpg
|
||||
Content-Type: 52312
|
||||
|
||||
<binary JPEG data>
|
||||
|
||||
(binary JPEG data)
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
@@ -2338,6 +2349,112 @@ Here's an example showing how to attach an APK file:
|
||||
<figcaption>File attachment sent from an external URL</figcaption>
|
||||
</figure>
|
||||
|
||||
## Icons
|
||||
_Supported on:_ :material-android:
|
||||
|
||||
You can include an icon that will appear next to the text of the notification. Simply pass the `X-Icon` header or query
|
||||
parameter (or its alias `Icon`) to specify the URL that the icon is located at. The client will automatically download
|
||||
the icon (unless it is already cached locally, and less than 24 hours old), and show it in the notification. Icons are
|
||||
cached locally in the client until the notification is deleted. **Only JPEG and PNG images are supported at this time**.
|
||||
|
||||
Here's an example showing how to include an icon:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl \
|
||||
-H "Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
|
||||
-H "Title: Kodi: Resuming Playback" \
|
||||
-H "Tags: arrow_forward" \
|
||||
-d "The Wire, S01E01" \
|
||||
ntfy.sh/tvshows
|
||||
```
|
||||
|
||||
=== "ntfy CLI"
|
||||
```
|
||||
ntfy publish \
|
||||
--icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" \
|
||||
--title="Kodi: Resuming Playback" \
|
||||
--tags="arrow_forward" \
|
||||
tvshows \
|
||||
"The Wire, S01E01"
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
POST /tvshows HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png
|
||||
Tags: arrow_forward
|
||||
Title: Kodi: Resuming Playback
|
||||
|
||||
The Wire, S01E01
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
fetch('https://ntfy.sh/tvshows', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Icon': 'https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png',
|
||||
'Title': 'Kodi: Resuming Playback',
|
||||
'Tags': 'arrow_forward'
|
||||
},
|
||||
body: "The Wire, S01E01"
|
||||
})
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/tvshows", strings.NewReader("The Wire, S01E01"))
|
||||
req.Header.Set("Icon", "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png")
|
||||
req.Header.Set("Tags", "arrow_forward")
|
||||
req.Header.Set("Title", "Kodi: Resuming Playback")
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$uri = "https://ntfy.sh/tvshows"
|
||||
$headers = @{ Title"="Kodi: Resuming Playback"
|
||||
Tags="arrow_forward"
|
||||
Icon="https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png" }
|
||||
$body = "The Wire, S01E01"
|
||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.sh/tvshows",
|
||||
data="The Wire, S01E01",
|
||||
headers={
|
||||
"Title": "Kodi: Resuming Playback",
|
||||
"Tags": "arrow_forward",
|
||||
"Icon": "https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png"
|
||||
})
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.sh/tvshows', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'PUT',
|
||||
'header' =>
|
||||
"Content-Type: text/plain\r\n" . // Does not matter
|
||||
"Title: Kodi: Resuming Playback\r\n" .
|
||||
"Tags: arrow_forward\r\n" .
|
||||
"Icon: https://styles.redditmedia.com/t5_32uhe/styles/communityIcon_xnt6chtnr2j21.png",
|
||||
],
|
||||
'content' => "The Wire, S01E01"
|
||||
]));
|
||||
```
|
||||
|
||||
Here's an example of how it will look on Android:
|
||||
|
||||
<figure markdown>
|
||||
{ width=500 }
|
||||
<figcaption>Custom icon from an external URL</figcaption>
|
||||
</figure>
|
||||
|
||||
## E-mail notifications
|
||||
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||
|
||||
@@ -2479,16 +2596,23 @@ title `You've Got Mail` to topic `sometopic` (see [ntfy.sh/sometopic](https://nt
|
||||
### Authentication
|
||||
Depending on whether the server is configured to support [access control](config.md#access-control), some topics
|
||||
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
|
||||
To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
||||
with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing
|
||||
your password.
|
||||
To publish/subscribe to protected topics, you can:
|
||||
|
||||
Here's a simple example:
|
||||
* Use [basic auth](#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
|
||||
* or use the [`auth` query parameter](#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw`
|
||||
|
||||
!!! warning
|
||||
Base64 only encodes username and password. It **is not encrypting it**. For your self-hosted server,
|
||||
**be sure to use HTTPS to avoid eavesdropping** and exposing your password.
|
||||
|
||||
#### Basic auth
|
||||
Here's an example using [Basic auth](https://en.wikipedia.org/wiki/Basic_access_authentication), with a user `testuser`
|
||||
and password `fakepassword`:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl \
|
||||
-u phil:mypass \
|
||||
-u testuser:fakepassword \
|
||||
-d "Look ma, with auth" \
|
||||
https://ntfy.example.com/mysecrets
|
||||
```
|
||||
@@ -2496,7 +2620,7 @@ Here's a simple example:
|
||||
=== "ntfy CLI"
|
||||
```
|
||||
ntfy publish \
|
||||
-u phil:mypass \
|
||||
-u testuser:fakepassword \
|
||||
ntfy.example.com/mysecrets \
|
||||
"Look ma, with auth"
|
||||
```
|
||||
@@ -2505,7 +2629,7 @@ Here's a simple example:
|
||||
``` http
|
||||
POST /mysecrets HTTP/1.1
|
||||
Host: ntfy.example.com
|
||||
Authorization: Basic cGhpbDpteXBhc3M=
|
||||
Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
|
||||
|
||||
Look ma, with auth
|
||||
```
|
||||
@@ -2516,7 +2640,7 @@ Here's a simple example:
|
||||
method: 'POST', // PUT works too
|
||||
body: 'Look ma, with auth',
|
||||
headers: {
|
||||
'Authorization': 'Basic cGhpbDpteXBhc3M='
|
||||
'Authorization': 'Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk'
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -2525,14 +2649,14 @@ Here's a simple example:
|
||||
``` go
|
||||
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
|
||||
strings.NewReader("Look ma, with auth"))
|
||||
req.Header.Set("Authorization", "Basic cGhpbDpteXBhc3M=")
|
||||
req.Header.Set("Authorization", "Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk")
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$uri = "https://ntfy.example.com/mysecrets"
|
||||
$credentials = 'username:password'
|
||||
$credentials = 'testuser:fakepassword'
|
||||
$encodedCredentials = [convert]::ToBase64String([text.Encoding]::UTF8.GetBytes($credentials))
|
||||
$headers = @{Authorization="Basic $encodedCredentials"}
|
||||
$message = "Look ma, with auth"
|
||||
@@ -2544,7 +2668,7 @@ Here's a simple example:
|
||||
requests.post("https://ntfy.example.com/mysecrets",
|
||||
data="Look ma, with auth",
|
||||
headers={
|
||||
"Authorization": "Basic cGhpbDpteXBhc3M="
|
||||
"Authorization": "Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk"
|
||||
})
|
||||
```
|
||||
|
||||
@@ -2555,12 +2679,113 @@ Here's a simple example:
|
||||
'method' => 'POST', // PUT also works
|
||||
'header' =>
|
||||
'Content-Type: text/plain\r\n' .
|
||||
'Authorization: Basic cGhpbDpteXBhc3M=',
|
||||
'Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk',
|
||||
'content' => 'Look ma, with auth'
|
||||
]
|
||||
]));
|
||||
```
|
||||
|
||||
To generate the `Authorization` header, use **standard base64** to encode the colon-separated `<username>:<password>`
|
||||
and prepend the word `Basic`, i.e. `Authorization: Basic base64(<username>:<password>)`. Here's some pseudo-code that
|
||||
hopefully explains it better:
|
||||
|
||||
```
|
||||
username = "testuser"
|
||||
password = "fakepassword"
|
||||
authHeader = "Basic " + base64(username + ":" + password) // -> Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
|
||||
```
|
||||
|
||||
The following command will generate the appropriate value for you on *nix systems:
|
||||
|
||||
```
|
||||
echo "Basic $(echo -n 'testuser:fakepassword' | base64)"
|
||||
```
|
||||
|
||||
#### Query param
|
||||
Here's an example using the `auth` query parameter:
|
||||
|
||||
=== "Command line (curl)"
|
||||
```
|
||||
curl \
|
||||
-d "Look ma, with auth" \
|
||||
"https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
|
||||
```
|
||||
|
||||
=== "ntfy CLI"
|
||||
```
|
||||
ntfy publish \
|
||||
-u testuser:fakepassword \
|
||||
ntfy.example.com/mysecrets \
|
||||
"Look ma, with auth"
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
POST /mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw HTTP/1.1
|
||||
Host: ntfy.example.com
|
||||
|
||||
Look ma, with auth
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
``` javascript
|
||||
fetch('https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw', {
|
||||
method: 'POST', // PUT works too
|
||||
body: 'Look ma, with auth'
|
||||
})
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
``` go
|
||||
req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw",
|
||||
strings.NewReader("Look ma, with auth"))
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$uri = "https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw"
|
||||
$message = "Look ma, with auth"
|
||||
Invoke-RestMethod -Uri $uri -Body $message -Method "Post" -UseBasicParsing
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.post("https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw",
|
||||
data="Look ma, with auth"
|
||||
```
|
||||
|
||||
=== "PHP"
|
||||
``` php-inline
|
||||
file_get_contents('https://ntfy.example.com/mysecrets?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw', false, stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST', // PUT also works
|
||||
'header' => 'Content-Type: text/plain',
|
||||
'content' => 'Look ma, with auth'
|
||||
]
|
||||
]));
|
||||
```
|
||||
|
||||
To generate the value of the `auth` parameter, encode the value of the `Authorization` header (see anove) using
|
||||
**raw base64 encoding** (like base64, but strip any trailing `=`). Here's some pseudo-code that hopefully
|
||||
explains it better:
|
||||
|
||||
```
|
||||
username = "testuser"
|
||||
password = "fakepassword"
|
||||
authHeader = "Basic " + base64(username + ":" + password) // -> Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
|
||||
authParam = base64_raw(authHeader) // -> QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw (no trailing =)
|
||||
|
||||
// If your language does not have a function to encode raw base64, simply use normal base64
|
||||
// and REMOVE TRAILING "=" characters.
|
||||
```
|
||||
|
||||
The following command will generate the appropriate value for you on *nix systems:
|
||||
|
||||
```
|
||||
echo -n "Basic `echo -n 'testuser:fakepassword' | base64`" | base64 | tr -d '='
|
||||
```
|
||||
|
||||
### Message caching
|
||||
!!! info
|
||||
If `Cache: no` is used, messages will only be delivered to connected subscribers, and won't be re-delivered if a
|
||||
@@ -2793,6 +3018,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
|
||||
| `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) |
|
||||
| `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) |
|
||||
| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment |
|
||||
| `X-Icon` | `Icon` | URL to use as notification [icon](#icons) |
|
||||
| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client |
|
||||
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
|
||||
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
||||
|
||||
216
docs/releases.md
216
docs/releases.md
@@ -2,18 +2,151 @@
|
||||
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).
|
||||
|
||||
<!--
|
||||
|
||||
## ntfy Android app v1.14.0 (UNRELEASED)
|
||||
## ntfy server v1.30.1
|
||||
Released December 23, 2022 🎅
|
||||
|
||||
**Features:**
|
||||
|
||||
* Polling is now done with since=<id> API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165))
|
||||
* 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))
|
||||
|
||||
## 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:**
|
||||
|
||||
* 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
|
||||
Released November 12, 2022
|
||||
|
||||
This release adds the ability to add rate limit exemptions for IP ranges instead of just specific IP addresses. It also fixes
|
||||
a few bugs in the web app and the CLI and adds lots of new examples and install instructions.
|
||||
|
||||
Thanks to [some love on HN](https://news.ycombinator.com/item?id=33517944), we got so many new ntfy users trying out ntfy
|
||||
and joining the [chat rooms](https://github.com/binwiederhier/ntfy#chat--forum). **Welcome to the ntfy community to all of you!**
|
||||
We also got a ton of new **[sponsors and donations](https://github.com/sponsors/binwiederhier)** 💸, which is amazing. I'd like to thank
|
||||
all of you for believing in the project, and for helping me pay the server cost. The HN spike increased the AWS cost quite a bit.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Allow IP CIDRs in `visitor-request-limit-exempt-hosts` ([#423](https://github.com/binwiederhier/ntfy/issues/423), thanks to [@karmanyaahm](https://github.com/karmanyaahm))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Subscriptions can now have a display name ([#370](https://github.com/binwiederhier/ntfy/issues/370), thanks to [@tfheen](https://github.com/tfheen) for reporting)
|
||||
* Bump Go version to Go 18.x ([#422](https://github.com/binwiederhier/ntfy/issues/422))
|
||||
* Web: Strip trailing slash when subscribing ([#428](https://github.com/binwiederhier/ntfy/issues/428), thanks to [@raining1123](https://github.com/raining1123) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
|
||||
* Web: Strip trailing slash after server URL in publish dialog ([#441](https://github.com/binwiederhier/ntfy/issues/441), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Allow empty passwords in `client.yml` ([#374](https://github.com/binwiederhier/ntfy/issues/374), thanks to [@cyqsimon](https://github.com/cyqsimon) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
|
||||
* `ntfy pub` will now use default username and password from `client.yml` ([#431](https://github.com/binwiederhier/ntfy/issues/431), thanks to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
* Make `ntfy sub` work with `NTFY_USER` env variable ([#447](https://github.com/binwiederhier/ntfy/pull/447), thanks to [SuperSandro2000](https://github.com/SuperSandro2000))
|
||||
* Web: Disallow GET/HEAD requests with body in actions ([#468](https://github.com/binwiederhier/ntfy/issues/468), thanks to [@ollien](https://github.com/ollien))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Updated developer docs, bump nodejs and go version ([#414](https://github.com/binwiederhier/ntfy/issues/414), thanks to [@YJSoft](https://github.com/YJSoft) for reporting)
|
||||
* Officially document `?auth=..` query parameter ([#433](https://github.com/binwiederhier/ntfy/pull/433), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Added Rundeck example ([#427](https://github.com/binwiederhier/ntfy/pull/427), thanks to [@demogorgonz](https://github.com/demogorgonz))
|
||||
* Fix Debian installation instructions ([#237](https://github.com/binwiederhier/ntfy/issues/237), thanks to [@Joeharrison94](https://github.com/Joeharrison94) for reporting)
|
||||
* Updated [example](https://ntfy.sh/docs/examples/#gatus) with official [Gatus](https://github.com/TwiN/gatus) integration (thanks to [@TwiN](https://github.com/TwiN))
|
||||
* Added [Kubernetes install instructions](https://ntfy.sh/docs/install/#kubernetes) ([#452](https://github.com/binwiederhier/ntfy/pull/452), thanks to [@gmemstr](https://github.com/gmemstr))
|
||||
* Added [additional NixOS links for self-hosting](https://ntfy.sh/docs/install/#nixos-nix) ([#462](https://github.com/binwiederhier/ntfy/pull/462), thanks to [@wamserma](https://github.com/wamserma))
|
||||
* Added additional [more secure nginx config example](https://ntfy.sh/docs/config/#nginxapache2caddy) ([#451](https://github.com/binwiederhier/ntfy/pull/451), thanks to [SuperSandro2000](https://github.com/SuperSandro2000))
|
||||
* Minor fixes in the config table ([#470](https://github.com/binwiederhier/ntfy/pull/470), thanks to [snh](https://github.com/snh))
|
||||
* Fix broken link ([#476](https://github.com/binwiederhier/ntfy/pull/476), thanks to [@shuuji3](https://github.com/shuuji3))
|
||||
|
||||
**Additional translations:**
|
||||
|
||||
* Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/))
|
||||
|
||||
**Sponsorships:**:
|
||||
|
||||
Thank you to the amazing folks who decided to [sponsor ntfy](https://github.com/sponsors/binwiederhier). Thank you for
|
||||
helping carry the cost of the public server and developer licenses, and more importantly: Thank you for believing in ntfy!
|
||||
You guys rock!
|
||||
|
||||
A list of all the sponsors can be found in the [README](https://github.com/binwiederhier/ntfy/blob/main/README.md).
|
||||
|
||||
## ntfy Android app v1.14.0
|
||||
Released September 27, 2022
|
||||
|
||||
This release adds the ability to set a custom icon to each notification, as well as a display name to subscriptions. We
|
||||
also moved the action buttons in the detail view to a more logical place, fixed a bunch of bugs, and added four more
|
||||
languages. Hurray!
|
||||
|
||||
**Features:**
|
||||
|
||||
* Subscriptions can now have a display name ([#313](https://github.com/binwiederhier/ntfy/issues/313), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Display name for UnifiedPush subscriptions ([#355](https://github.com/binwiederhier/ntfy/issues/355), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Polling is now done with `since=<id>` API, which makes deduping easier ([#165](https://github.com/binwiederhier/ntfy/issues/165))
|
||||
* Turned JSON stream deprecation banner into "Use WebSockets" banner (no ticket)
|
||||
* Move action buttons in notification cards ([#236](https://github.com/binwiederhier/ntfy/issues/236), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Long-click selecting of notifications doesn't scoll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Long-click selecting of notifications doesn't scroll to the top anymore ([#235](https://github.com/binwiederhier/ntfy/issues/235), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Add attachment and click URL extras to MESSAGE_RECEIVED broadcast ([#329](https://github.com/binwiederhier/ntfy/issues/329), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Accessibility: Clear/choose service URL button in base URL dropdown now has a label ([#292](https://github.com/binwiederhier/ntfy/issues/292), thanks to [@mhameed](https://github.com/mhameed) for reporting)
|
||||
|
||||
@@ -21,14 +154,57 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
|
||||
* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/))
|
||||
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
|
||||
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
|
||||
|
||||
Thank you to [@wunter8](https://github.com/wunter8) for proactively picking up some Android tickets, and fixing them! You rock!
|
||||
|
||||
-->
|
||||
## ntfy server v1.28.0
|
||||
Released September 27, 2022
|
||||
|
||||
This release primarily adds icon support for the Android app, and adds a display name to subscriptions in the web app.
|
||||
Aside from that, we fixed a few random bugs, most importantly the `Priority` header bug that allows the use behind
|
||||
Cloudflare. We also added a ton of documentation. Most prominently, an [integrations + projects page](https://ntfy.sh/docs/integrations/).
|
||||
|
||||
As of now, I also have started accepting **[donations and sponsorships](https://github.com/sponsors/binwiederhier)** 💸.
|
||||
I would be very humbled if you consider donating.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Subscription display name for the web app ([#348](https://github.com/binwiederhier/ntfy/pull/348))
|
||||
* Allow setting socket permissions via `--listen-unix-mode` ([#356](https://github.com/binwiederhier/ntfy/pull/356), thanks to [@koro666](https://github.com/koro666))
|
||||
* Icons can be set for each individual notification ([#126](https://github.com/binwiederhier/ntfy/issues/126), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* CLI: Allow default username/password in `client.yml` ([#372](https://github.com/binwiederhier/ntfy/pull/372), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Build support for other Unix systems ([#393](https://github.com/binwiederhier/ntfy/pull/393), thanks to [@la-ninpre](https://github.com/la-ninpre))
|
||||
|
||||
**Bug fixes:**
|
||||
|
||||
* `ntfy user` commands don't work with `auth_file` but works with `auth-file` ([#344](https://github.com/binwiederhier/ntfy/issues/344), thanks to [@Histalek](https://github.com/Histalek) for reporting)
|
||||
* Ignore new draft HTTP `Priority` header ([#351](https://github.com/binwiederhier/ntfy/issues/351), thanks to [@ksurl](https://github.com/ksurl) for reporting)
|
||||
* Delete expired attachments based on mod time instead of DB entry to avoid races (no ticket)
|
||||
* Better logging for Matrix push key errors ([#384](https://github.com/binwiederhier/ntfy/pull/384), thanks to [@christophehenry](https://github.com/christophehenry))
|
||||
* Web: Switched "Pop" and "Pop Swoosh" sounds ([#352](https://github.com/binwiederhier/ntfy/issues/352), thanks to [@coma-toast](https://github.com/coma-toast) for reporting)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Added [integrations + projects page](https://ntfy.sh/docs/integrations/) (**so many integrations, whoa!**)
|
||||
* Added example for [UptimeRobot](https://ntfy.sh/docs/examples/#uptimerobot)
|
||||
* Fix some PowerShell publish docs ([#345](https://github.com/binwiederhier/ntfy/pull/345), thanks to [@noahpeltier](https://github.com/noahpeltier))
|
||||
* Clarified Docker install instructions ([#361](https://github.com/binwiederhier/ntfy/issues/361), thanks to [@barart](https://github.com/barart) for reporting)
|
||||
* Mismatched quotation marks ([#392](https://github.com/binwiederhier/ntfy/pull/392)], thanks to [@connorlanigan](https://github.com/connorlanigan))
|
||||
|
||||
**Additional translations:**
|
||||
|
||||
* Ukranian (thanks to [@v.kopitsa](https://hosted.weblate.org/user/v.kopitsa/))
|
||||
* Polish (thanks to [@Namax0r](https://hosted.weblate.org/user/Namax0r/))
|
||||
|
||||
## ntfy server v1.27.2
|
||||
Released June 23, 2022
|
||||
|
||||
This release brings two new CLI options to wait for a command to finish, or for a PID to exit. It also adds more detail
|
||||
to trace debug output. Aside from other bugs, it fixes a performance issue that occurred in large installations every
|
||||
minute or so, due to competing stats gathering (personal installations will likely be unaffected by this).
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket)
|
||||
@@ -36,7 +212,7 @@ Released June 23, 2022
|
||||
* Trace: Log entire HTTP request to simplify debugging (no ticket)
|
||||
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Fix slow requests due to excessive locking ([#338](https://github.com/binwiederhier/ntfy/issues/338))
|
||||
* Return HTTP 500 for `GET /_matrix/push/v1/notify` when `base-url` is not configured (no ticket)
|
||||
@@ -61,7 +237,7 @@ CLI is now available via Scoop, and ntfy is now natively supported in Uptime Kum
|
||||
* [Uptime Kuma](https://github.com/louislam/uptime-kuma) now allows publishing to ntfy ([uptime-kuma#1674](https://github.com/louislam/uptime-kuma/pull/1674), thanks to [@philippdormann](https://github.com/philippdormann))
|
||||
* Display ntfy version in `ntfy serve` command ([#314](https://github.com/binwiederhier/ntfy/issues/314), thanks to [@poblabs](https://github.com/poblabs))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Web app: Show "notifications not supported" alert on HTTP ([#323](https://github.com/binwiederhier/ntfy/issues/323), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
||||
* Use last address in `X-Forwarded-For` header as visitor address ([#328](https://github.com/binwiederhier/ntfy/issues/328))
|
||||
@@ -84,7 +260,7 @@ set your server as the default server for new topics.
|
||||
* Support for auth and user management ([#277](https://github.com/binwiederhier/ntfy/issues/277))
|
||||
* Ability to add default server ([#295](https://github.com/binwiederhier/ntfy/issues/295))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Add validation for selfhosted server URL ([#290](https://github.com/binwiederhier/ntfy/issues/290))
|
||||
|
||||
@@ -147,7 +323,7 @@ for details).
|
||||
* Cancel notifications when navigating to topic (no ticket)
|
||||
* iOS 14.0 support (no ticket, [PR#1](https://github.com/binwiederhier/ntfy-ios/pull/1), thanks to [@callum-99](https://github.com/callum-99))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267))
|
||||
|
||||
@@ -164,7 +340,7 @@ Apple development environment.
|
||||
* Add subscribe filter to query exact messages by ID (no ticket)
|
||||
* Support for `poll_request` messages to support [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for self-hosted servers (no ticket)
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Support emails without `Content-Type` ([#265](https://github.com/binwiederhier/ntfy/issues/265), thanks to [@dmbonsall](https://github.com/dmbonsall))
|
||||
|
||||
@@ -202,7 +378,7 @@ it adds support for APNs, the iOS messaging service. This is needed for the (soo
|
||||
* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
|
||||
* Add APNs config to Firebase messages to support [iOS app](https://github.com/binwiederhier/ntfy/issues/4) ([#247](https://github.com/binwiederhier/ntfy/pull/247), thanks to [@Copephobia](https://github.com/Copephobia))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Support underscores in server.yml config options ([#255](https://github.com/binwiederhier/ntfy/issues/255), thanks to [@ajdelgado](https://github.com/ajdelgado))
|
||||
* Force MAKEFLAGS to --jobs=1 in `Makefile` ([#257](https://github.com/binwiederhier/ntfy/pull/257), thanks to [@oddlama](https://github.com/oddlama))
|
||||
@@ -231,7 +407,7 @@ and custom icons. Aside from that, we've got tons of bug fixes as usual.
|
||||
* Per-subscription settings, custom subscription icons ([#155](https://github.com/binwiederhier/ntfy/issues/155), thanks to [@mztiq](https://github.com/mztiq) for reporting)
|
||||
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/175), thanks to [@cmeis](https://github.com/cmeis) for reporting)
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Accurate naming of "mute notifications" from "pause notifications" ([#224](https://github.com/binwiederhier/ntfy/issues/224), thanks to [@shadow00](https://github.com/shadow00) for reporting)
|
||||
* Make messages with links selectable ([#226](https://github.com/binwiederhier/ntfy/issues/226), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
|
||||
@@ -264,7 +440,7 @@ We've also improved the documentation a little and added translations for three
|
||||
* Better parsing of the user actions, allowing quotes (no ticket)
|
||||
* Add "mark as read" icon button to notification ([#243](https://github.com/binwiederhier/ntfy/pull/243), thanks to [@wunter8](https://github.com/wunter8))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* `Upgrade` header check is now case in-sensitive ([#228](https://github.com/binwiederhier/ntfy/issues/228), thanks to [@wunter8](https://github.com/wunter8) for finding it)
|
||||
* Made web app sounds quieter ([#222](https://github.com/binwiederhier/ntfy/issues/222))
|
||||
@@ -306,7 +482,7 @@ languages and fixed a ton of bugs.
|
||||
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
|
||||
* Channel settings option to configure DND override, sounds, etc. ([#91](https://github.com/binwiederhier/ntfy/issues/91))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Validate URLs when changing default server and server in user management ([#193](https://github.com/binwiederhier/ntfy/issues/193),
|
||||
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
|
||||
@@ -347,7 +523,7 @@ Limited support is available in the web app.
|
||||
* Added ARMv6 build ([#200](https://github.com/binwiederhier/ntfy/issues/200), thanks to [@jcrubioa](https://github.com/jcrubioa) for reporting)
|
||||
* Web app internationalization support 🇧🇬 🇩🇪 🇺🇸 🌎 ([#189](https://github.com/binwiederhier/ntfy/issues/189))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
|
||||
* Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis))
|
||||
@@ -387,7 +563,7 @@ Released Apr 7, 2022
|
||||
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
|
||||
[@StoyanDimitrov](https://github.com/StoyanDimitrov) for initiating things)
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* IllegalStateException: Failed to build unique file ([#177](https://github.com/binwiederhier/ntfy/issues/177), thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
|
||||
* SQLiteConstraintException: Crash during UP registration ([#185](https://github.com/binwiederhier/ntfy/issues/185))
|
||||
@@ -421,7 +597,7 @@ Released Apr 6, 2022
|
||||
|
||||
* Added message bar and publish dialog ([#196](https://github.com/binwiederhier/ntfy/issues/196))
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
|
||||
|
||||
@@ -437,7 +613,7 @@ Released Apr 6, 2022
|
||||
## ntfy server v1.19.0
|
||||
Released Mar 30, 2022
|
||||
|
||||
**Bugs:**
|
||||
**Bug fixes:**
|
||||
|
||||
* Do not pack binary with `upx` for armv7/arm64 due to `illegal instruction` errors ([#191](https://github.com/binwiederhier/ntfy/issues/191), thanks to [@iexos](https://github.com/iexos))
|
||||
* Do not allow comma in topic name in publish via GET endpoint (no ticket)
|
||||
|
||||
BIN
docs/static/img/android-screenshot-icon.png
vendored
Normal file
BIN
docs/static/img/android-screenshot-icon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/rundeck.png
vendored
Normal file
BIN
docs/static/img/rundeck.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/static/img/uptimerobot-setup.jpg
vendored
Normal file
BIN
docs/static/img/uptimerobot-setup.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/static/img/uptimerobot-test.jpg
vendored
Normal file
BIN
docs/static/img/uptimerobot-test.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -302,13 +302,12 @@ $ curl -s ntfy.sh/mytopic1,mytopic2/json
|
||||
### Authentication
|
||||
Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
|
||||
may be read/write protected so that only users with the correct credentials can subscribe or publish to them.
|
||||
To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
|
||||
with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing
|
||||
your password.
|
||||
To publish/subscribe to protected topics, you can:
|
||||
|
||||
```
|
||||
curl -u phil:mypass -s "https://ntfy.example.com/mytopic/json"
|
||||
```
|
||||
* Use [basic auth](../publish.md#basic-auth), e.g. `Authorization: Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk`
|
||||
* or use the [`auth` query parameter](../publish.md#query-param), e.g. `?auth=QmFzaWMgZEdWemRIVnpaWEk2Wm1GclpYQmhjM04zYjNKaw`
|
||||
|
||||
Please refer to the [publishing documentation](../publish.md#authentication) for additional details.
|
||||
|
||||
## JSON message format
|
||||
Both the [`/json` endpoint](#subscribe-as-json-stream) and the [`/sse` endpoint](#subscribe-as-sse-stream) return a JSON
|
||||
|
||||
@@ -254,6 +254,14 @@ I hope this shows how powerful this command is. Here's a short video that demons
|
||||
<figcaption>Execute all the things</figcaption>
|
||||
</figure>
|
||||
|
||||
If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
|
||||
`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
|
||||
override the defaults.
|
||||
|
||||
!!! warning
|
||||
Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
|
||||
be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||
|
||||
### Using the systemd service
|
||||
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)
|
||||
|
||||
69
go.mod
69
go.mod
@@ -1,58 +1,61 @@
|
||||
module heckel.io/ntfy
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.6.1 // indirect
|
||||
cloud.google.com/go/storage v1.23.0 // indirect
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||
cloud.google.com/go/storage v1.28.1 // indirect
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/emersion/go-smtp v0.15.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.13
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.10.2
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 // indirect
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
google.golang.org/api v0.85.0
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.23.7
|
||||
golang.org/x/crypto v0.4.0
|
||||
golang.org/x/oauth2 v0.3.0 // indirect
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.3.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/api v0.105.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require github.com/pkg/errors v0.9.1 // indirect
|
||||
|
||||
require firebase.google.com/go/v4 v4.8.0
|
||||
require firebase.google.com/go/v4 v4.10.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.102.1 // indirect
|
||||
cloud.google.com/go/compute v1.7.0 // indirect
|
||||
cloud.google.com/go/iam v0.3.0 // indirect
|
||||
cloud.google.com/go v0.107.0 // indirect
|
||||
cloud.google.com/go/compute v1.14.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.9.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.3.0 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
|
||||
github.com/googleapis/go-type-adapters v1.0.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220622184535-263ec571b305 // indirect
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694 // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect
|
||||
google.golang.org/grpc v1.51.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
716
go.sum
716
go.sum
@@ -1,711 +1,213 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
|
||||
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
|
||||
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
||||
cloud.google.com/go v0.102.1 h1:vpK6iQWv/2uUeFJth4/cBHsQAGjn1iIE6AAlxipRaA0=
|
||||
cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
|
||||
cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
|
||||
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
|
||||
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
|
||||
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
|
||||
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
|
||||
cloud.google.com/go/compute v1.7.0 h1:v/k9Eueb8aAJ0vZuxKMrgm6kPhCLZU9HxFU+AFDs9Uk=
|
||||
cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
|
||||
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
|
||||
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
|
||||
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||
cloud.google.com/go/storage v1.23.0 h1:wWRIaDURQA8xxHguFCshYepGlrWIrbBnAmc7wfg07qY=
|
||||
cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
firebase.google.com/go/v4 v4.8.0 h1:ooJqjFEh1G6DQ5+wyb/RAXAgku0E2RzJeH6WauSpWSo=
|
||||
firebase.google.com/go/v4 v4.8.0/go.mod h1:y+j6xX7BgBco/XaN+YExIBVm6pzvYutheDV3nprvbWc=
|
||||
cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww=
|
||||
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
|
||||
cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
|
||||
cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
|
||||
cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
|
||||
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
|
||||
cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
|
||||
cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
|
||||
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.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
|
||||
cloud.google.com/go/iam v0.9.0 h1:bK6Or6mxhuL8lnj1i9j0yMo2wE/IeTO2cWlfUrf/TZs=
|
||||
cloud.google.com/go/iam v0.9.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.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc=
|
||||
cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI=
|
||||
cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y=
|
||||
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=
|
||||
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/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/MicahParks/keyfunc v1.7.0 h1:LBd4tBj6FwGs2S4GXniQbgrG0PXzIldyGDKWch8slhg=
|
||||
github.com/MicahParks/keyfunc v1.7.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
|
||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/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/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
|
||||
github.com/emersion/go-smtp v0.15.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.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.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
|
||||
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
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/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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/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/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
|
||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
|
||||
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.1/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/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||
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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
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/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y=
|
||||
github.com/urfave/cli/v2 v2.10.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo=
|
||||
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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
|
||||
github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
||||
github.com/urfave/cli/v2 v2.23.7/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/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220622184535-263ec571b305 h1:dAgbJ2SP4jD6XYfMNLVj0BF21jo2PjChrtGaAvF5M3I=
|
||||
golang.org/x/net v0.0.0-20220622184535-263ec571b305/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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0=
|
||||
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
|
||||
golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
|
||||
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.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-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
|
||||
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
|
||||
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
|
||||
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
|
||||
google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
|
||||
google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
|
||||
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
|
||||
google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80=
|
||||
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
|
||||
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
|
||||
google.golang.org/api v0.73.0/go.mod h1:lbd/q6BRFJbdpV6OUCXstVeiI5mL/d3/WifG7iNKnjI=
|
||||
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
|
||||
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
|
||||
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
|
||||
google.golang.org/api v0.85.0 h1:8rJoHuRxx+vCmZtAO/3k1dRLvYNVyTJtZ5oaFZvhgvc=
|
||||
google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
|
||||
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=
|
||||
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
|
||||
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
|
||||
google.golang.org/api v0.105.0 h1:t6P9Jj+6XTn4U9I2wycQai6Q/Kz7iOT+QzjJ3G2V4x8=
|
||||
google.golang.org/api v0.105.0/go.mod h1:qh7eD5FJks5+BcE+cjBIm6Gz8vioK7EHvnlniqXBnqI=
|
||||
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine/v2 v2.0.1 h1:jTGfiRmR5qoInpT3CXJ72GJEB4owDGEKN+xRDA6ekBY=
|
||||
google.golang.org/appengine/v2 v2.0.1/go.mod h1:XgltgQxPOF3ShivrVrZyfvYCx8Dunh73bKjUuXUZb8Q=
|
||||
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
|
||||
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
||||
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
||||
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694 h1:itnFmgk4Ls5nT+mYO2ZK6F0DpKsGZLhB5BB9y5ZL2HA=
|
||||
google.golang.org/genproto v0.0.0-20220623142657-077d458a5694/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
|
||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
|
||||
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
|
||||
google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
|
||||
google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
|
||||
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-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -714,31 +216,17 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
14
log/log.go
14
log/log.go
@@ -40,32 +40,32 @@ var (
|
||||
)
|
||||
|
||||
// Trace prints the given message, if the current log level is TRACE
|
||||
func Trace(message string, v ...interface{}) {
|
||||
func Trace(message string, v ...any) {
|
||||
logIf(TraceLevel, message, v...)
|
||||
}
|
||||
|
||||
// Debug prints the given message, if the current log level is DEBUG or lower
|
||||
func Debug(message string, v ...interface{}) {
|
||||
func Debug(message string, v ...any) {
|
||||
logIf(DebugLevel, message, v...)
|
||||
}
|
||||
|
||||
// Info prints the given message, if the current log level is INFO or lower
|
||||
func Info(message string, v ...interface{}) {
|
||||
func Info(message string, v ...any) {
|
||||
logIf(InfoLevel, message, v...)
|
||||
}
|
||||
|
||||
// Warn prints the given message, if the current log level is WARN or lower
|
||||
func Warn(message string, v ...interface{}) {
|
||||
func Warn(message string, v ...any) {
|
||||
logIf(WarnLevel, message, v...)
|
||||
}
|
||||
|
||||
// Error prints the given message, if the current log level is ERROR or lower
|
||||
func Error(message string, v ...interface{}) {
|
||||
func Error(message string, v ...any) {
|
||||
logIf(ErrorLevel, message, v...)
|
||||
}
|
||||
|
||||
// Fatal prints the given message, and exits the program
|
||||
func Fatal(v ...interface{}) {
|
||||
func Fatal(v ...any) {
|
||||
log.Fatalln(v...)
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func IsDebug() bool {
|
||||
return Loggable(DebugLevel)
|
||||
}
|
||||
|
||||
func logIf(l Level, message string, v ...interface{}) {
|
||||
func logIf(l Level, message string, v ...any) {
|
||||
if CurrentLevel() <= l {
|
||||
log.Printf(l.String()+" "+message, v...)
|
||||
}
|
||||
|
||||
@@ -85,9 +85,11 @@ nav:
|
||||
- "Other things":
|
||||
- "FAQs": faq.md
|
||||
- "Examples": examples.md
|
||||
- "Integrations + projects": integrations.md
|
||||
- "Release notes": releases.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
- "Emojis 🥳 🎉": emojis.md
|
||||
- "Known issues": known-issues.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
- "Development": develop.md
|
||||
- "Privacy policy": privacy.md
|
||||
|
||||
|
||||
@@ -60,13 +60,13 @@ func parseActions(s string) (actions []*action, err error) {
|
||||
return nil, fmt.Errorf("only %d actions allowed", actionsMax)
|
||||
}
|
||||
for _, action := range actions {
|
||||
if !util.InStringList(actionsAll, action.Action) {
|
||||
if !util.Contains(actionsAll, action.Action) {
|
||||
return nil, fmt.Errorf("parameter 'action' cannot be '%s', valid values are 'view', 'broadcast' and 'http'", action.Action)
|
||||
} else if action.Label == "" {
|
||||
return nil, fmt.Errorf("parameter 'label' is required")
|
||||
} else if util.InStringList(actionsWithURL, action.Action) && action.URL == "" {
|
||||
} else if util.Contains(actionsWithURL, action.Action) && action.URL == "" {
|
||||
return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action)
|
||||
} else if action.Action == actionHTTP && util.InStringList([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
|
||||
} else if action.Action == actionHTTP && util.Contains([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
|
||||
return nil, fmt.Errorf("parameter 'body' cannot be set if method is %s", action.Method)
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ func parseActionsFromJSON(s string) ([]*action, error) {
|
||||
// https://ntfy.sh/docs/publish/#action-buttons), into an array of actions.
|
||||
//
|
||||
// It can parse an actions string like this:
|
||||
// view, "Look ma, commas and \"quotes\" too", url=https://..; action=broadcast, ...
|
||||
//
|
||||
// view, "Look ma, commas and \"quotes\" too", url=https://..; action=broadcast, ...
|
||||
//
|
||||
// It works by advancing the position ("pos") through the input string ("input").
|
||||
//
|
||||
@@ -96,10 +97,11 @@ func parseActionsFromJSON(s string) ([]*action, error) {
|
||||
// though it does not use state functions at all.
|
||||
//
|
||||
// Other resources:
|
||||
// https://adampresley.github.io/2015/04/12/writing-a-lexer-and-parser-in-go-part-1.html
|
||||
// https://github.com/adampresley/sample-ini-parser/blob/master/services/lexer/lexer/Lexer.go
|
||||
// https://github.com/benbjohnson/sql-parser/blob/master/scanner.go
|
||||
// https://blog.gopheracademy.com/advent-2014/parsers-lexers/
|
||||
//
|
||||
// https://adampresley.github.io/2015/04/12/writing-a-lexer-and-parser-in-go-part-1.html
|
||||
// https://github.com/adampresley/sample-ini-parser/blob/master/services/lexer/lexer/Lexer.go
|
||||
// https://github.com/benbjohnson/sql-parser/blob/master/scanner.go
|
||||
// https://blog.gopheracademy.com/advent-2014/parsers-lexers/
|
||||
func parseActionsFromSimple(s string) ([]*action, error) {
|
||||
if !utf8.ValidString(s) {
|
||||
return nil, errors.New("invalid utf-8 string")
|
||||
@@ -154,7 +156,7 @@ func populateAction(newAction *action, section int, key, value string) error {
|
||||
key = "action"
|
||||
} else if key == "" && section == 1 {
|
||||
key = "label"
|
||||
} else if key == "" && section == 2 && util.InStringList(actionsWithURL, newAction.Action) {
|
||||
} else if key == "" && section == 2 && util.Contains(actionsWithURL, newAction.Action) {
|
||||
key = "url"
|
||||
}
|
||||
|
||||
@@ -176,7 +178,7 @@ func populateAction(newAction *action, section int, key, value string) error {
|
||||
newAction.Label = value
|
||||
case "clear":
|
||||
lvalue := strings.ToLower(value)
|
||||
if !util.InStringList([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
|
||||
if !util.Contains([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
|
||||
return fmt.Errorf("parameter 'clear' cannot be '%s', only boolean values are allowed (true/yes/1/false/no/0)", value)
|
||||
}
|
||||
newAction.Clear = lvalue == "true" || lvalue == "yes" || lvalue == "1"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -52,12 +54,15 @@ type Config struct {
|
||||
ListenHTTP string
|
||||
ListenHTTPS string
|
||||
ListenUnix string
|
||||
ListenUnixMode fs.FileMode
|
||||
KeyFile string
|
||||
CertFile string
|
||||
FirebaseKeyFile string
|
||||
CacheFile string
|
||||
CacheDuration time.Duration
|
||||
CacheStartupQueries string
|
||||
CacheBatchSize int
|
||||
CacheBatchTimeout time.Duration
|
||||
AuthFile string
|
||||
AuthDefaultRead bool
|
||||
AuthDefaultWrite bool
|
||||
@@ -90,7 +95,7 @@ type Config struct {
|
||||
VisitorAttachmentDailyBandwidthLimit int
|
||||
VisitorRequestLimitBurst int
|
||||
VisitorRequestLimitReplenish time.Duration
|
||||
VisitorRequestExemptIPAddrs []string
|
||||
VisitorRequestExemptIPAddrs []netip.Prefix
|
||||
VisitorEmailLimitBurst int
|
||||
VisitorEmailLimitReplenish time.Duration
|
||||
BehindProxy bool
|
||||
@@ -105,11 +110,14 @@ func NewConfig() *Config {
|
||||
ListenHTTP: DefaultListenHTTP,
|
||||
ListenHTTPS: "",
|
||||
ListenUnix: "",
|
||||
ListenUnixMode: 0,
|
||||
KeyFile: "",
|
||||
CertFile: "",
|
||||
FirebaseKeyFile: "",
|
||||
CacheFile: "",
|
||||
CacheDuration: DefaultCacheDuration,
|
||||
CacheBatchSize: 0,
|
||||
CacheBatchTimeout: 0,
|
||||
AuthFile: "",
|
||||
AuthDefaultRead: true,
|
||||
AuthDefaultWrite: true,
|
||||
@@ -132,7 +140,7 @@ func NewConfig() *Config {
|
||||
VisitorAttachmentDailyBandwidthLimit: DefaultVisitorAttachmentDailyBandwidthLimit,
|
||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||
VisitorRequestExemptIPAddrs: make([]string, 0),
|
||||
VisitorRequestExemptIPAddrs: make([]netip.Prefix, 0),
|
||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||
BehindProxy: false,
|
||||
|
||||
@@ -23,7 +23,7 @@ func (e errHTTP) JSON() string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func wrapErrHTTP(err *errHTTP, message string, args ...interface{}) *errHTTP {
|
||||
func wrapErrHTTP(err *errHTTP, message string, args ...any) *errHTTP {
|
||||
return &errHTTP{
|
||||
Code: err.Code,
|
||||
HTTPCode: err.HTTPCode,
|
||||
@@ -52,6 +52,7 @@ var (
|
||||
errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"}
|
||||
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
||||
errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"}
|
||||
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
|
||||
|
||||
@@ -2,16 +2,18 @@ package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
fileIDRegex = regexp.MustCompile(`^[-_A-Za-z0-9]+$`)
|
||||
fileIDRegex = regexp.MustCompile(fmt.Sprintf(`^[-_A-Za-z0-9]{%d}$`, messageIDLength))
|
||||
errInvalidFileID = errors.New("invalid file ID")
|
||||
errFileExists = errors.New("file exists")
|
||||
)
|
||||
@@ -88,6 +90,25 @@ func (c *fileCache) Remove(ids ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expired returns a list of file IDs for expired files
|
||||
func (c *fileCache) Expired(olderThan time.Time) ([]string, error) {
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ids []string
|
||||
for _, e := range entries {
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(olderThan) && fileIDRegex.MatchString(e.Name()) {
|
||||
ids = append(ids, e.Name())
|
||||
}
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (c *fileCache) Size() int64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -16,10 +17,10 @@ var (
|
||||
|
||||
func TestFileCache_Write_Success(t *testing.T) {
|
||||
dir, c := newTestFileCache(t)
|
||||
size, err := c.Write("abc", strings.NewReader("normal file"), util.NewFixedLimiter(999))
|
||||
size, err := c.Write("abcdefghijkl", strings.NewReader("normal file"), util.NewFixedLimiter(999))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(11), size)
|
||||
require.Equal(t, "normal file", readFile(t, dir+"/abc"))
|
||||
require.Equal(t, "normal file", readFile(t, dir+"/abcdefghijkl"))
|
||||
require.Equal(t, int64(11), c.Size())
|
||||
require.Equal(t, int64(10229), c.Remaining())
|
||||
}
|
||||
@@ -27,18 +28,18 @@ func TestFileCache_Write_Success(t *testing.T) {
|
||||
func TestFileCache_Write_Remove_Success(t *testing.T) {
|
||||
dir, c := newTestFileCache(t) // max = 10k (10240), each = 1k (1024)
|
||||
for i := 0; i < 10; i++ { // 10x999 = 9990
|
||||
size, err := c.Write(fmt.Sprintf("abc%d", i), bytes.NewReader(make([]byte, 999)))
|
||||
size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(make([]byte, 999)))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(999), size)
|
||||
}
|
||||
require.Equal(t, int64(9990), c.Size())
|
||||
require.Equal(t, int64(250), c.Remaining())
|
||||
require.FileExists(t, dir+"/abc1")
|
||||
require.FileExists(t, dir+"/abc5")
|
||||
require.FileExists(t, dir+"/abcdefghijk1")
|
||||
require.FileExists(t, dir+"/abcdefghijk5")
|
||||
|
||||
require.Nil(t, c.Remove("abc1", "abc5"))
|
||||
require.NoFileExists(t, dir+"/abc1")
|
||||
require.NoFileExists(t, dir+"/abc5")
|
||||
require.Nil(t, c.Remove("abcdefghijk1", "abcdefghijk5"))
|
||||
require.NoFileExists(t, dir+"/abcdefghijk1")
|
||||
require.NoFileExists(t, dir+"/abcdefghijk5")
|
||||
require.Equal(t, int64(7992), c.Size())
|
||||
require.Equal(t, int64(2248), c.Remaining())
|
||||
}
|
||||
@@ -46,27 +47,50 @@ func TestFileCache_Write_Remove_Success(t *testing.T) {
|
||||
func TestFileCache_Write_FailedTotalSizeLimit(t *testing.T) {
|
||||
dir, c := newTestFileCache(t)
|
||||
for i := 0; i < 10; i++ {
|
||||
size, err := c.Write(fmt.Sprintf("abc%d", i), bytes.NewReader(oneKilobyteArray))
|
||||
size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(oneKilobyteArray))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(1024), size)
|
||||
}
|
||||
_, err := c.Write("abc11", bytes.NewReader(oneKilobyteArray))
|
||||
_, err := c.Write("abcdefghijkX", bytes.NewReader(oneKilobyteArray))
|
||||
require.Equal(t, util.ErrLimitReached, err)
|
||||
require.NoFileExists(t, dir+"/abc11")
|
||||
require.NoFileExists(t, dir+"/abcdefghijkX")
|
||||
}
|
||||
|
||||
func TestFileCache_Write_FailedFileSizeLimit(t *testing.T) {
|
||||
dir, c := newTestFileCache(t)
|
||||
_, err := c.Write("abc", bytes.NewReader(make([]byte, 1025)))
|
||||
_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1025)))
|
||||
require.Equal(t, util.ErrLimitReached, err)
|
||||
require.NoFileExists(t, dir+"/abc")
|
||||
require.NoFileExists(t, dir+"/abcdefghijkl")
|
||||
}
|
||||
|
||||
func TestFileCache_Write_FailedAdditionalLimiter(t *testing.T) {
|
||||
dir, c := newTestFileCache(t)
|
||||
_, err := c.Write("abc", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000))
|
||||
_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), util.NewFixedLimiter(1000))
|
||||
require.Equal(t, util.ErrLimitReached, err)
|
||||
require.NoFileExists(t, dir+"/abc")
|
||||
require.NoFileExists(t, dir+"/abcdefghijkl")
|
||||
}
|
||||
|
||||
func TestFileCache_RemoveExpired(t *testing.T) {
|
||||
dir, c := newTestFileCache(t)
|
||||
_, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)))
|
||||
require.Nil(t, err)
|
||||
_, err = c.Write("notdeleted12", bytes.NewReader(make([]byte, 1001)))
|
||||
require.Nil(t, err)
|
||||
|
||||
modTime := time.Now().Add(-1 * 4 * time.Hour)
|
||||
require.Nil(t, os.Chtimes(dir+"/abcdefghijkl", modTime, modTime))
|
||||
|
||||
olderThan := time.Now().Add(-1 * 3 * time.Hour)
|
||||
ids, err := c.Expired(olderThan)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []string{"abcdefghijkl"}, ids)
|
||||
require.Nil(t, c.Remove(ids...))
|
||||
require.NoFileExists(t, dir+"/abcdefghijkl")
|
||||
require.FileExists(t, dir+"/notdeleted12")
|
||||
|
||||
ids, err = c.Expired(olderThan)
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, ids)
|
||||
}
|
||||
|
||||
func newTestFileCache(t *testing.T) (dir string, cache *fileCache) {
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,6 +32,7 @@ const (
|
||||
priority INT NOT NULL,
|
||||
tags TEXT NOT NULL,
|
||||
click TEXT NOT NULL,
|
||||
icon TEXT NOT NULL,
|
||||
actions TEXT NOT NULL,
|
||||
attachment_name TEXT NOT NULL,
|
||||
attachment_type TEXT NOT NULL,
|
||||
@@ -41,41 +44,42 @@ const (
|
||||
published INT NOT NULL
|
||||
);
|
||||
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);
|
||||
COMMIT;
|
||||
`
|
||||
insertMessageQuery = `
|
||||
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
|
||||
selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
|
||||
selectMessagesSinceTimeQuery = `
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND time >= ? AND published = 1
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceTimeIncludeScheduledQuery = `
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND time >= ?
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceIDQuery = `
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND id > ? AND published = 1
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesSinceIDIncludeScheduledQuery = `
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
FROM messages
|
||||
WHERE topic = ? AND (id > ? OR published = 0)
|
||||
ORDER BY time, id
|
||||
`
|
||||
selectMessagesDueQuery = `
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
SELECT mid, time, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
|
||||
FROM messages
|
||||
WHERE time <= ? AND published = 0
|
||||
ORDER BY time, id
|
||||
@@ -85,12 +89,11 @@ const (
|
||||
selectMessageCountPerTopicQuery = `SELECT topic, COUNT(*) FROM messages GROUP BY topic`
|
||||
selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic`
|
||||
selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE sender = ? AND attachment_expires >= ?`
|
||||
selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?`
|
||||
)
|
||||
|
||||
// Schema management queries
|
||||
const (
|
||||
currentSchemaVersion = 7
|
||||
currentSchemaVersion = 9
|
||||
createSchemaVersionTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||
id INT PRIMARY KEY,
|
||||
@@ -178,15 +181,26 @@ const (
|
||||
migrate6To7AlterMessagesTableQuery = `
|
||||
ALTER TABLE messages RENAME COLUMN attachment_owner TO sender;
|
||||
`
|
||||
|
||||
// 7 -> 8
|
||||
migrate7To8AlterMessagesTableQuery = `
|
||||
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 {
|
||||
db *sql.DB
|
||||
nop bool
|
||||
db *sql.DB
|
||||
queue *util.BatchingQueue[*message]
|
||||
nop bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -194,21 +208,28 @@ func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, e
|
||||
if err := setupCacheDB(db, startupQueries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &messageCache{
|
||||
db: db,
|
||||
nop: nop,
|
||||
}, nil
|
||||
var queue *util.BatchingQueue[*message]
|
||||
if batchSize > 0 || batchTimeout > 0 {
|
||||
queue = util.NewBatchingQueue[*message](batchSize, batchTimeout)
|
||||
}
|
||||
cache := &messageCache{
|
||||
db: db,
|
||||
queue: queue,
|
||||
nop: nop,
|
||||
}
|
||||
go cache.processMessageBatches()
|
||||
return cache, nil
|
||||
}
|
||||
|
||||
// newMemCache creates an in-memory cache
|
||||
func newMemCache() (*messageCache, error) {
|
||||
return newSqliteCache(createMemoryFilename(), "", false)
|
||||
return newSqliteCache(createMemoryFilename(), "", 0, 0, false)
|
||||
}
|
||||
|
||||
// newNopCache creates an in-memory cache that discards all messages;
|
||||
// it is always empty and can be used if caching is entirely disabled
|
||||
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.
|
||||
@@ -221,19 +242,36 @@ func createMemoryFilename() string {
|
||||
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 {
|
||||
if c.queue != nil {
|
||||
c.queue.Enqueue(m)
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
if c.nop {
|
||||
return nil
|
||||
}
|
||||
if len(ms) == 0 {
|
||||
return nil
|
||||
}
|
||||
start := time.Now()
|
||||
tx, err := c.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
stmt, err := tx.Prepare(insertMessageQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
for _, m := range ms {
|
||||
if m.Event != messageEvent {
|
||||
return errUnexpectedMessageType
|
||||
@@ -257,8 +295,11 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
}
|
||||
actionsStr = string(actionsBytes)
|
||||
}
|
||||
_, err := tx.Exec(
|
||||
insertMessageQuery,
|
||||
var sender string
|
||||
if m.Sender.IsValid() {
|
||||
sender = m.Sender.String()
|
||||
}
|
||||
_, err := stmt.Exec(
|
||||
m.ID,
|
||||
m.Time,
|
||||
m.Topic,
|
||||
@@ -267,13 +308,14 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
m.Priority,
|
||||
tags,
|
||||
m.Click,
|
||||
m.Icon,
|
||||
actionsStr,
|
||||
attachmentName,
|
||||
attachmentType,
|
||||
attachmentSize,
|
||||
attachmentExpires,
|
||||
attachmentURL,
|
||||
m.Sender,
|
||||
sender,
|
||||
m.Encoding,
|
||||
published,
|
||||
)
|
||||
@@ -281,7 +323,12 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
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) {
|
||||
@@ -387,8 +434,12 @@ func (c *messageCache) Topics() (map[string]*topic, error) {
|
||||
}
|
||||
|
||||
func (c *messageCache) Prune(olderThan time.Time) error {
|
||||
_, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix())
|
||||
return err
|
||||
start := time.Now()
|
||||
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) {
|
||||
@@ -409,24 +460,15 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (c *messageCache) AttachmentsExpired() ([]string, error) {
|
||||
rows, err := c.db.Query(selectAttachmentsExpiredQuery, time.Now().Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (c *messageCache) processMessageBatches() {
|
||||
if c.queue == nil {
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
ids := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return nil, err
|
||||
for messages := range c.queue.Dequeue() {
|
||||
if err := c.addMessages(messages); err != nil {
|
||||
log.Error("Cache: %s", err.Error())
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
@@ -435,7 +477,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
for rows.Next() {
|
||||
var timestamp, attachmentSize, attachmentExpires int64
|
||||
var priority int
|
||||
var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string
|
||||
var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string
|
||||
err := rows.Scan(
|
||||
&id,
|
||||
×tamp,
|
||||
@@ -445,6 +487,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
&priority,
|
||||
&tagsStr,
|
||||
&click,
|
||||
&icon,
|
||||
&actionsStr,
|
||||
&attachmentName,
|
||||
&attachmentType,
|
||||
@@ -467,6 +510,10 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
senderIP, err := netip.ParseAddr(sender)
|
||||
if err != nil {
|
||||
senderIP = netip.Addr{} // if no IP stored in database, return invalid address
|
||||
}
|
||||
var att *attachment
|
||||
if attachmentName != "" && attachmentURL != "" {
|
||||
att = &attachment{
|
||||
@@ -487,9 +534,10 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
Priority: priority,
|
||||
Tags: tags,
|
||||
Click: click,
|
||||
Icon: icon,
|
||||
Actions: actions,
|
||||
Attachment: att,
|
||||
Sender: sender,
|
||||
Sender: senderIP, // Must parse assuming database must be correct
|
||||
Encoding: encoding,
|
||||
})
|
||||
}
|
||||
@@ -545,6 +593,10 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
|
||||
return migrateFrom5(db)
|
||||
} else if schemaVersion == 6 {
|
||||
return migrateFrom6(db)
|
||||
} else if schemaVersion == 7 {
|
||||
return migrateFrom7(db)
|
||||
} else if schemaVersion == 8 {
|
||||
return migrateFrom8(db)
|
||||
}
|
||||
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
||||
}
|
||||
@@ -639,5 +691,27 @@ func migrateFrom6(db *sql.DB) error {
|
||||
if _, err := db.Exec(updateSchemaVersion, 7); err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateFrom7(db)
|
||||
}
|
||||
|
||||
func migrateFrom7(db *sql.DB) error {
|
||||
log.Info("Migrating cache database schema: from 7 to 8")
|
||||
if _, err := db.Exec(migrate7To8AlterMessagesTableQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Exec(updateSchemaVersion, 8); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,11 +3,17 @@ package server
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
exampleIP1234 = netip.MustParseAddr("1.2.3.4")
|
||||
)
|
||||
|
||||
func TestSqliteCache_Messages(t *testing.T) {
|
||||
@@ -281,7 +287,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires1 := time.Now().Add(-4 * time.Hour).Unix()
|
||||
m := newDefaultMessage("mytopic", "flower for you")
|
||||
m.ID = "m1"
|
||||
m.Sender = "1.2.3.4"
|
||||
m.Sender = exampleIP1234
|
||||
m.Attachment = &attachment{
|
||||
Name: "flower.jpg",
|
||||
Type: "image/jpeg",
|
||||
@@ -294,7 +300,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires2 := time.Now().Add(2 * time.Hour).Unix() // Future
|
||||
m = newDefaultMessage("mytopic", "sending you a car")
|
||||
m.ID = "m2"
|
||||
m.Sender = "1.2.3.4"
|
||||
m.Sender = exampleIP1234
|
||||
m.Attachment = &attachment{
|
||||
Name: "car.jpg",
|
||||
Type: "image/jpeg",
|
||||
@@ -307,7 +313,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
expires3 := time.Now().Add(1 * time.Hour).Unix() // Future
|
||||
m = newDefaultMessage("another-topic", "sending you another car")
|
||||
m.ID = "m3"
|
||||
m.Sender = "1.2.3.4"
|
||||
m.Sender = exampleIP1234
|
||||
m.Attachment = &attachment{
|
||||
Name: "another-car.jpg",
|
||||
Type: "image/jpeg",
|
||||
@@ -327,7 +333,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
require.Equal(t, int64(5000), messages[0].Attachment.Size)
|
||||
require.Equal(t, expires1, messages[0].Attachment.Expires)
|
||||
require.Equal(t, "https://ntfy.sh/file/AbDeFgJhal.jpg", messages[0].Attachment.URL)
|
||||
require.Equal(t, "1.2.3.4", messages[0].Sender)
|
||||
require.Equal(t, "1.2.3.4", messages[0].Sender.String())
|
||||
|
||||
require.Equal(t, "sending you a car", messages[1].Message)
|
||||
require.Equal(t, "car.jpg", messages[1].Attachment.Name)
|
||||
@@ -335,7 +341,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
require.Equal(t, int64(10000), messages[1].Attachment.Size)
|
||||
require.Equal(t, expires2, messages[1].Attachment.Expires)
|
||||
require.Equal(t, "https://ntfy.sh/file/aCaRURL.jpg", messages[1].Attachment.URL)
|
||||
require.Equal(t, "1.2.3.4", messages[1].Sender)
|
||||
require.Equal(t, "1.2.3.4", messages[1].Sender.String())
|
||||
|
||||
size, err := c.AttachmentBytesUsed("1.2.3.4")
|
||||
require.Nil(t, err)
|
||||
@@ -344,10 +350,6 @@ func testCacheAttachments(t *testing.T, c *messageCache) {
|
||||
size, err = c.AttachmentBytesUsed("5.6.7.8")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(0), size)
|
||||
|
||||
ids, err := c.AttachmentsExpired()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []string{"m1"}, ids)
|
||||
}
|
||||
|
||||
func TestSqliteCache_Migration_From0(t *testing.T) {
|
||||
@@ -448,7 +450,7 @@ func TestSqliteCache_StartupQueries_WAL(t *testing.T) {
|
||||
startupQueries := `pragma journal_mode = WAL;
|
||||
pragma synchronous = normal;
|
||||
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, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
||||
require.FileExists(t, filename)
|
||||
@@ -459,7 +461,7 @@ pragma temp_store = memory;`
|
||||
func TestSqliteCache_StartupQueries_None(t *testing.T) {
|
||||
filename := newSqliteTestCacheFile(t)
|
||||
startupQueries := ""
|
||||
db, err := newSqliteCache(filename, startupQueries, false)
|
||||
db, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message")))
|
||||
require.FileExists(t, filename)
|
||||
@@ -470,10 +472,33 @@ func TestSqliteCache_StartupQueries_None(t *testing.T) {
|
||||
func TestSqliteCache_StartupQueries_Fail(t *testing.T) {
|
||||
filename := newSqliteTestCacheFile(t)
|
||||
startupQueries := `xx error`
|
||||
_, err := newSqliteCache(filename, startupQueries, false)
|
||||
_, err := newSqliteCache(filename, startupQueries, 0, 0, false)
|
||||
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) {
|
||||
rows, err := db.Query(`SELECT version FROM schemaVersion`)
|
||||
require.Nil(t, err)
|
||||
@@ -499,7 +524,7 @@ func TestMemCache_NopCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func newSqliteTestCache(t *testing.T) *messageCache {
|
||||
c, err := newSqliteCache(newSqliteTestCacheFile(t), "", false)
|
||||
c, err := newSqliteCache(newSqliteTestCacheFile(t), "", 0, 0, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -511,7 +536,7 @@ func newSqliteTestCacheFile(t *testing.T) string {
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -42,7 +43,7 @@ type Server struct {
|
||||
smtpServerBackend *smtpBackend
|
||||
smtpSender mailer
|
||||
topics map[string]*topic
|
||||
visitors map[string]*visitor
|
||||
visitors map[netip.Addr]*visitor
|
||||
firebaseClient *firebaseClient
|
||||
messages int64
|
||||
auth auth.Auther
|
||||
@@ -74,7 +75,7 @@ var (
|
||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
||||
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
|
||||
attachURLRegex = regexp.MustCompile(`^https?://`)
|
||||
urlRegex = regexp.MustCompile(`^https?://`)
|
||||
|
||||
//go:embed site
|
||||
webFs embed.FS
|
||||
@@ -150,7 +151,7 @@ func New(conf *Config) (*Server, error) {
|
||||
smtpSender: mailer,
|
||||
topics: topics,
|
||||
auth: auther,
|
||||
visitors: make(map[string]*visitor),
|
||||
visitors: make(map[netip.Addr]*visitor),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -158,7 +159,7 @@ func createMessageCache(conf *Config) (*messageCache, error) {
|
||||
if conf.CacheDuration == 0 {
|
||||
return newNopCache()
|
||||
} else if conf.CacheFile != "" {
|
||||
return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, false)
|
||||
return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, conf.CacheBatchSize, conf.CacheBatchTimeout, false)
|
||||
}
|
||||
return newMemCache()
|
||||
}
|
||||
@@ -204,9 +205,18 @@ func (s *Server) Run() error {
|
||||
os.Remove(s.config.ListenUnix)
|
||||
s.unixListener, err = net.Listen("unix", s.config.ListenUnix)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
defer s.unixListener.Close()
|
||||
if s.config.ListenUnixMode > 0 {
|
||||
if err := os.Chmod(s.config.ListenUnix, s.config.ListenUnixMode); err != nil {
|
||||
s.mu.Unlock()
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
httpServer := &http.Server{Handler: mux}
|
||||
errChan <- httpServer.Serve(s.unixListener)
|
||||
@@ -481,6 +491,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
|
||||
log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
|
||||
}
|
||||
if cache {
|
||||
log.Debug("%s Adding message to cache", logMessagePrefix(v, m))
|
||||
if err := s.messageCache.AddMessage(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -559,6 +570,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
|
||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||
m.Title = readParam(r, "x-title", "title", "t")
|
||||
m.Click = readParam(r, "x-click", "click")
|
||||
icon := readParam(r, "x-icon", "icon")
|
||||
filename := readParam(r, "x-filename", "filename", "file", "f")
|
||||
attach := readParam(r, "x-attach", "attach", "a")
|
||||
if attach != "" || filename != "" {
|
||||
@@ -568,7 +580,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
|
||||
m.Attachment.Name = filename
|
||||
}
|
||||
if attach != "" {
|
||||
if !attachURLRegex.MatchString(attach) {
|
||||
if !urlRegex.MatchString(attach) {
|
||||
return false, false, "", false, errHTTPBadRequestAttachmentURLInvalid
|
||||
}
|
||||
m.Attachment.URL = attach
|
||||
@@ -585,6 +597,12 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
|
||||
m.Attachment.Name = "attachment"
|
||||
}
|
||||
}
|
||||
if icon != "" {
|
||||
if !urlRegex.MatchString(icon) {
|
||||
return false, false, "", false, errHTTPBadRequestIconURLInvalid
|
||||
}
|
||||
m.Icon = icon
|
||||
}
|
||||
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||
if email != "" {
|
||||
if err := v.EmailAllowed(); err != nil {
|
||||
@@ -651,18 +669,18 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
|
||||
|
||||
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
||||
//
|
||||
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
||||
// If a message is flagged as poll request, the body does not matter and is discarded
|
||||
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
||||
// If body is binary, encode as base64, if not do not encode
|
||||
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
||||
// Body must be a message, because we attached an external URL
|
||||
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
||||
// Body must be attachment, because we passed a filename
|
||||
// 5. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||
// 6. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is > message limit, treat it as an attachment
|
||||
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
||||
// If a message is flagged as poll request, the body does not matter and is discarded
|
||||
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
||||
// If body is binary, encode as base64, if not do not encode
|
||||
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
||||
// Body must be a message, because we attached an external URL
|
||||
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
||||
// Body must be attachment, because we passed a filename
|
||||
// 5. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||
// 6. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is > message limit, treat it as an attachment
|
||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
|
||||
if m.Event == pollRequestEvent { // Case 1
|
||||
return s.handleBodyDiscard(body)
|
||||
@@ -1072,7 +1090,7 @@ func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
|
||||
defer s.mu.Unlock()
|
||||
topics := make([]*topic, 0)
|
||||
for _, id := range ids {
|
||||
if util.InStringList(disallowedTopics, id) {
|
||||
if util.Contains(disallowedTopics, id) {
|
||||
return nil, errHTTPBadRequestTopicDisallowed
|
||||
}
|
||||
if _, ok := s.topics[id]; !ok {
|
||||
@@ -1107,8 +1125,9 @@ func (s *Server) updateStatsAndPrune() {
|
||||
log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors)
|
||||
|
||||
// Delete expired attachments
|
||||
if s.fileCache != nil {
|
||||
ids, err := s.messageCache.AttachmentsExpired()
|
||||
if s.fileCache != nil && s.config.AttachmentExpiryDuration > 0 {
|
||||
olderThan := time.Now().Add(-1 * s.config.AttachmentExpiryDuration)
|
||||
ids, err := s.fileCache.Expired(olderThan)
|
||||
if err != nil {
|
||||
log.Warn("Error retrieving expired attachments: %s", err.Error())
|
||||
} else if len(ids) > 0 {
|
||||
@@ -1202,7 +1221,7 @@ func (s *Server) runFirebaseKeepaliver() {
|
||||
if s.firebaseClient == nil {
|
||||
return
|
||||
}
|
||||
v := newVisitor(s.config, s.messageCache, "0.0.0.0") // Background process, not a real visitor
|
||||
v := newVisitor(s.config, s.messageCache, netip.IPv4Unspecified()) // Background process, not a real visitor, uses IP 0.0.0.0
|
||||
for {
|
||||
select {
|
||||
case <-time.After(s.config.FirebaseKeepaliveInterval):
|
||||
@@ -1269,7 +1288,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
||||
|
||||
func (s *Server) limitRequests(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if util.InStringList(s.config.VisitorRequestExemptIPAddrs, v.ip) {
|
||||
if util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) {
|
||||
return next(w, r, v)
|
||||
} else if err := v.RequestAllowed(); err != nil {
|
||||
return errHTTPTooManyRequestsLimitRequests
|
||||
@@ -1326,6 +1345,9 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||
if m.Click != "" {
|
||||
r.Header.Set("X-Click", m.Click)
|
||||
}
|
||||
if m.Icon != "" {
|
||||
r.Header.Set("X-Icon", m.Icon)
|
||||
}
|
||||
if len(m.Actions) > 0 {
|
||||
actionsStr, err := json.Marshal(m.Actions)
|
||||
if err != nil {
|
||||
@@ -1416,21 +1438,35 @@ func extractUserPass(r *http.Request) (username string, password string, ok bool
|
||||
// This function was taken from https://www.alexedwards.net/blog/how-to-rate-limit-http-requests (MIT).
|
||||
func (s *Server) visitor(r *http.Request) *visitor {
|
||||
remoteAddr := r.RemoteAddr
|
||||
ip, _, err := net.SplitHostPort(remoteAddr)
|
||||
addrPort, err := netip.ParseAddrPort(remoteAddr)
|
||||
ip := addrPort.Addr()
|
||||
if err != nil {
|
||||
ip = remoteAddr // This should not happen in real life; only in tests.
|
||||
// This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified
|
||||
ip, err = netip.ParseAddr(remoteAddr)
|
||||
if err != nil {
|
||||
ip = netip.IPv4Unspecified()
|
||||
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")) != "" {
|
||||
// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy,
|
||||
// only the right-most address can be trusted (as this is the one added by our proxy server).
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
|
||||
ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",")
|
||||
ip = strings.TrimSpace(util.LastString(ips, remoteAddr))
|
||||
realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
|
||||
if err != nil {
|
||||
log.Error("invalid IP address %s received in X-Forwarded-For header: %s", ip, err.Error())
|
||||
// Fall back to regular remote address if X-Forwarded-For is damaged
|
||||
} else {
|
||||
ip = realIP
|
||||
}
|
||||
}
|
||||
return s.visitorFromIP(ip)
|
||||
}
|
||||
|
||||
func (s *Server) visitorFromIP(ip string) *visitor {
|
||||
func (s *Server) visitorFromIP(ip netip.Addr) *visitor {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
v, exists := s.visitors[ip]
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
# This can be useful to avoid port issues on local systems, and to simplify permissions.
|
||||
#
|
||||
# listen-unix: <socket-path>
|
||||
# listen-unix-mode: <linux permissions, e.g. 0700>
|
||||
|
||||
# Path to the private key & cert file for the HTTPS web server. Not used if "listen-https" is not set.
|
||||
#
|
||||
@@ -52,6 +53,12 @@
|
||||
# pragma journal_mode = WAL;
|
||||
# pragma synchronous = normal;
|
||||
# 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:
|
||||
# Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package
|
||||
@@ -64,6 +71,8 @@
|
||||
# cache-file: <filename>
|
||||
# cache-duration: "12h"
|
||||
# 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
|
||||
# the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs.
|
||||
@@ -172,8 +181,9 @@
|
||||
# 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-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
|
||||
# exempt from request rate limiting; hostnames are resolved at the time the server is started
|
||||
# - 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.
|
||||
# Example: "1.2.3.4,ntfy.example.com,8.7.6.0/24"
|
||||
#
|
||||
# visitor-request-limit-burst: 60
|
||||
# visitor-request-limit-replenish: "5s"
|
||||
|
||||
@@ -148,6 +148,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
||||
"priority": fmt.Sprintf("%d", m.Priority),
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": m.Click,
|
||||
"icon": m.Icon,
|
||||
"title": m.Title,
|
||||
"message": m.Message,
|
||||
"encoding": m.Encoding,
|
||||
@@ -216,7 +217,7 @@ func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
|
||||
// We must set the Alert struct ("alert"), and we need to set MutableContent ("mutable-content"), so the Notification Service
|
||||
// Extension in iOS can modify the message.
|
||||
func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSConfig {
|
||||
apnsData := make(map[string]interface{})
|
||||
apnsData := make(map[string]any)
|
||||
for k, v := range data {
|
||||
apnsData[k] = v
|
||||
}
|
||||
@@ -240,7 +241,7 @@ func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSCo
|
||||
//
|
||||
// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
|
||||
func createAPNSBackgroundConfig(data map[string]string) *messaging.APNSConfig {
|
||||
apnsData := make(map[string]interface{})
|
||||
apnsData := make(map[string]any)
|
||||
for k, v := range data {
|
||||
apnsData[k] = v
|
||||
}
|
||||
|
||||
@@ -3,13 +3,15 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/auth"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/auth"
|
||||
)
|
||||
|
||||
type testAuther struct {
|
||||
@@ -71,7 +73,7 @@ func TestToFirebaseMessage_Keepalive(t *testing.T) {
|
||||
Aps: &messaging.Aps{
|
||||
ContentAvailable: true,
|
||||
},
|
||||
CustomData: map[string]interface{}{
|
||||
CustomData: map[string]any{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
@@ -102,7 +104,7 @@ func TestToFirebaseMessage_Open(t *testing.T) {
|
||||
Aps: &messaging.Aps{
|
||||
ContentAvailable: true,
|
||||
},
|
||||
CustomData: map[string]interface{}{
|
||||
CustomData: map[string]any{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
@@ -123,6 +125,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
m.Priority = 4
|
||||
m.Tags = []string{"tag 1", "tag2"}
|
||||
m.Click = "https://google.com"
|
||||
m.Icon = "https://ntfy.sh/static/img/ntfy.png"
|
||||
m.Title = "some title"
|
||||
m.Actions = []*action{
|
||||
{
|
||||
@@ -165,7 +168,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
Body: "this is a message",
|
||||
},
|
||||
},
|
||||
CustomData: map[string]interface{}{
|
||||
CustomData: map[string]any{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": "message",
|
||||
@@ -173,6 +176,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
"priority": "4",
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": "https://google.com",
|
||||
"icon": "https://ntfy.sh/static/img/ntfy.png",
|
||||
"title": "some title",
|
||||
"message": "this is a message",
|
||||
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||
@@ -193,6 +197,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
||||
"priority": "4",
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": "https://google.com",
|
||||
"icon": "https://ntfy.sh/static/img/ntfy.png",
|
||||
"title": "some title",
|
||||
"message": "this is a message",
|
||||
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||
@@ -239,7 +244,7 @@ func TestToFirebaseMessage_PollRequest(t *testing.T) {
|
||||
Body: "New message",
|
||||
},
|
||||
},
|
||||
CustomData: map[string]interface{}{
|
||||
CustomData: map[string]any{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": "poll_request",
|
||||
@@ -319,7 +324,7 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
|
||||
func TestToFirebaseSender_Abuse(t *testing.T) {
|
||||
sender := &testFirebaseSender{allowed: 2}
|
||||
client := newFirebaseClient(sender, &testAuther{})
|
||||
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), "1.2.3.4")
|
||||
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"))
|
||||
|
||||
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
|
||||
require.Equal(t, 1, len(sender.Messages()))
|
||||
|
||||
@@ -47,17 +47,17 @@ import (
|
||||
//
|
||||
// From the message, we only require the "pushkey", as it represents our target topic URL.
|
||||
// A message may look like this (excerpt):
|
||||
// {
|
||||
// "notification": {
|
||||
// "devices": [
|
||||
// {
|
||||
// "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1",
|
||||
// ...
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "notification": {
|
||||
// "devices": [
|
||||
// {
|
||||
// "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1",
|
||||
// ...
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
type matrixRequest struct {
|
||||
Notification *struct {
|
||||
Devices []*struct {
|
||||
@@ -96,14 +96,13 @@ const (
|
||||
//
|
||||
// It basically converts a Matrix push gatewqy request:
|
||||
//
|
||||
// POST /_matrix/push/v1/notify HTTP/1.1
|
||||
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
|
||||
// POST /_matrix/push/v1/notify HTTP/1.1
|
||||
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
|
||||
//
|
||||
// to a ntfy request, looking like this:
|
||||
//
|
||||
// POST /upDAHJKFFDFD?up=1 HTTP/1.1
|
||||
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
|
||||
//
|
||||
// POST /upDAHJKFFDFD?up=1 HTTP/1.1
|
||||
// { "notification": { "devices": [ { "pushkey": "https://ntfy.sh/upDAHJKFFDFD?up=1", ... } ] } }
|
||||
func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int) (*http.Request, error) {
|
||||
if baseURL == "" {
|
||||
return nil, errHTTPInternalErrorMissingBaseURL
|
||||
@@ -124,7 +123,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
|
||||
}
|
||||
pushKey := m.Notification.Devices[0].PushKey // We ignore other devices for now, see discussion in #316
|
||||
if !strings.HasPrefix(pushKey, baseURL+"/") {
|
||||
return nil, &errMatrix{pushKey: pushKey, err: errHTTPBadRequestMatrixPushkeyBaseURLMismatch}
|
||||
return nil, &errMatrix{pushKey: pushKey, err: wrapErrHTTP(errHTTPBadRequestMatrixPushkeyBaseURLMismatch, "received push key: %s, configured base URL: %s", pushKey, baseURL)}
|
||||
}
|
||||
newRequest, err := http.NewRequest(http.MethodPost, pushKey, io.NopCloser(bytes.NewReader(body.PeekedBytes)))
|
||||
if err != nil {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatrix_NewRequestFromMatrixJSON_Success(t *testing.T) {
|
||||
@@ -56,7 +58,7 @@ func TestMatrix_NewRequestFromMatrixJSON_MismatchingPushKey(t *testing.T) {
|
||||
_, err := newRequestFromMatrixJSON(r, baseURL, maxLength)
|
||||
matrixErr, ok := err.(*errMatrix)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, errHTTPBadRequestMatrixPushkeyBaseURLMismatch, matrixErr.err)
|
||||
require.Equal(t, "invalid request: push key must be prefixed with base URL, received push key: https://ntfy.example.com/upABCDEFGHI?up=1, configured base URL: https://ntfy.sh", matrixErr.err.Error())
|
||||
require.Equal(t, "https://ntfy.example.com/upABCDEFGHI?up=1", matrixErr.pushKey)
|
||||
}
|
||||
|
||||
@@ -70,7 +72,7 @@ func TestMatrix_WriteMatrixDiscoveryResponse(t *testing.T) {
|
||||
func TestMatrix_WriteMatrixError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", nil)
|
||||
v := newVisitor(newTestConfig(t), nil, "1.2.3.4")
|
||||
v := newVisitor(newTestConfig(t), nil, netip.MustParseAddr("1.2.3.4"))
|
||||
require.Nil(t, writeMatrixError(w, r, v, &errMatrix{"https://ntfy.example.com/upABCDEFGHI?up=1", errHTTPBadRequestMatrixPushkeyBaseURLMismatch}))
|
||||
require.Equal(t, 200, w.Result().StatusCode)
|
||||
require.Equal(t, `{"rejected":["https://ntfy.example.com/upABCDEFGHI?up=1"]}`+"\n", w.Body.String())
|
||||
|
||||
@@ -6,18 +6,20 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/auth"
|
||||
"heckel.io/ntfy/util"
|
||||
@@ -66,6 +68,8 @@ func TestServer_PublishWithFirebase(t *testing.T) {
|
||||
msg1 := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg1.ID)
|
||||
require.Equal(t, "my first message", msg1.Message)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Firebase publishing happens
|
||||
require.Equal(t, 1, len(sender.Messages()))
|
||||
require.Equal(t, "my first message", sender.Messages()[0].Data["message"])
|
||||
require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body)
|
||||
@@ -290,13 +294,13 @@ func TestServer_PublishAt(t *testing.T) {
|
||||
messages = toMessages(t, response.Body.String())
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
require.Equal(t, "", messages[0].Sender) // Never return the sender!
|
||||
require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender!
|
||||
|
||||
messages, err := s.messageCache.Messages("mytopic", sinceAllMessages, true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(messages))
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
require.Equal(t, "9.9.9.9", messages[0].Sender) // It's stored in the DB though!
|
||||
require.Equal(t, "9.9.9.9", messages[0].Sender.String()) // It's stored in the DB though!
|
||||
}
|
||||
|
||||
func TestServer_PublishAtWithCacheError(t *testing.T) {
|
||||
@@ -812,7 +816,7 @@ func TestServer_PublishTooRequests_Defaults(t *testing.T) {
|
||||
|
||||
func TestServer_PublishTooRequests_Defaults_ExemptHosts(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.VisitorRequestExemptIPAddrs = []string{"9.9.9.9"} // see request()
|
||||
c.VisitorRequestExemptIPAddrs = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request()
|
||||
s := newTestServer(t, c)
|
||||
for i := 0; i < 65; i++ { // > 60
|
||||
response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil)
|
||||
@@ -832,7 +836,7 @@ func TestServer_PublishTooRequests_ShortReplenish(t *testing.T) {
|
||||
response := request(t, s, "PUT", "/mytopic", "message", nil)
|
||||
require.Equal(t, 429, response.Code)
|
||||
|
||||
time.Sleep(510 * time.Millisecond)
|
||||
time.Sleep(520 * time.Millisecond)
|
||||
response = request(t, s, "PUT", "/mytopic", "message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
}
|
||||
@@ -1044,7 +1048,7 @@ func TestServer_PublishAsJSON(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
|
||||
`"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` +
|
||||
`"delay":"30min"}`
|
||||
`"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}`
|
||||
response := request(t, s, "PUT", "/", body, nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
@@ -1056,6 +1060,8 @@ func TestServer_PublishAsJSON(t *testing.T) {
|
||||
require.Equal(t, "http://google.com", m.Attachment.URL)
|
||||
require.Equal(t, "google.pdf", m.Attachment.Name)
|
||||
require.Equal(t, "http://ntfy.sh", m.Click)
|
||||
require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
|
||||
|
||||
require.Equal(t, 4, m.Priority)
|
||||
require.True(t, m.Time > time.Now().Unix()+29*60)
|
||||
require.True(t, m.Time < time.Now().Unix()+31*60)
|
||||
@@ -1128,7 +1134,7 @@ func TestServer_PublishAttachment(t *testing.T) {
|
||||
require.Equal(t, int64(5000), msg.Attachment.Size)
|
||||
require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours
|
||||
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||
require.Equal(t, "", msg.Sender) // Should never be returned
|
||||
require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned
|
||||
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
|
||||
|
||||
// GET
|
||||
@@ -1164,7 +1170,7 @@ func TestServer_PublishAttachmentShortWithFilename(t *testing.T) {
|
||||
require.Equal(t, int64(21), msg.Attachment.Size)
|
||||
require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix())
|
||||
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||
require.Equal(t, "", msg.Sender) // Should never be returned
|
||||
require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned
|
||||
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))
|
||||
|
||||
path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
|
||||
@@ -1191,7 +1197,7 @@ func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) {
|
||||
require.Equal(t, "", msg.Attachment.Type)
|
||||
require.Equal(t, int64(0), msg.Attachment.Size)
|
||||
require.Equal(t, int64(0), msg.Attachment.Expires)
|
||||
require.Equal(t, "", msg.Sender)
|
||||
require.Equal(t, netip.Addr{}, msg.Sender)
|
||||
|
||||
// Slightly unrelated cross-test: make sure we don't add an owner for external attachments
|
||||
size, err := s.messageCache.AttachmentBytesUsed("127.0.0.1")
|
||||
@@ -1212,7 +1218,7 @@ func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) {
|
||||
require.Equal(t, "", msg.Attachment.Type)
|
||||
require.Equal(t, int64(0), msg.Attachment.Size)
|
||||
require.Equal(t, int64(0), msg.Attachment.Expires)
|
||||
require.Equal(t, "", msg.Sender)
|
||||
require.Equal(t, netip.Addr{}, msg.Sender)
|
||||
}
|
||||
|
||||
func TestServer_PublishAttachmentBadURL(t *testing.T) {
|
||||
@@ -1387,7 +1393,7 @@ func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty!
|
||||
v := s.visitor(r)
|
||||
require.Equal(t, "8.9.10.11", v.ip)
|
||||
require.Equal(t, "8.9.10.11", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
|
||||
@@ -1398,7 +1404,7 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.Header.Set("X-Forwarded-For", "1.1.1.1")
|
||||
v := s.visitor(r)
|
||||
require.Equal(t, "1.1.1.1", v.ip)
|
||||
require.Equal(t, "1.1.1.1", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
|
||||
@@ -1409,7 +1415,7 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ")
|
||||
v := s.visitor(r)
|
||||
require.Equal(t, "234.5.2.1", v.ip)
|
||||
require.Equal(t, "234.5.2.1", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *smtpSender) Send(v *visitor, m *message, to string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message, err := formatMail(s.config.BaseURL, v.ip, s.config.SMTPSenderFrom, to, m)
|
||||
message, err := formatMail(s.config.BaseURL, v.ip.String(), s.config.SMTPSenderFrom, to, m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
|
||||
nextTag:
|
||||
for _, t := range tags { // TODO Super inefficient; we should just create a .json file with a map
|
||||
for _, e := range emojis {
|
||||
if util.InStringList(e.Aliases, t) {
|
||||
if util.Contains(e.Aliases, t) {
|
||||
emojisOut = append(emojisOut, e.Emoji)
|
||||
continue nextTag
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"heckel.io/ntfy/util"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
// List of possible events
|
||||
@@ -29,10 +31,11 @@ type message struct {
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Click string `json:"click,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Actions []*action `json:"actions,omitempty"`
|
||||
Attachment *attachment `json:"attachment,omitempty"`
|
||||
PollID string `json:"poll_id,omitempty"`
|
||||
Sender string `json:"-"` // IP address of uploader, used for rate limiting
|
||||
Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
|
||||
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
||||
}
|
||||
|
||||
@@ -72,6 +75,7 @@ type publishMessage struct {
|
||||
Priority int `json:"priority"`
|
||||
Tags []string `json:"tags"`
|
||||
Click string `json:"click"`
|
||||
Icon string `json:"icon"`
|
||||
Actions []action `json:"actions"`
|
||||
Attach string `json:"attach"`
|
||||
Filename string `json:"filename"`
|
||||
@@ -174,7 +178,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
||||
for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") {
|
||||
priority, err := util.ParsePriority(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errHTTPBadRequestPriorityInvalid
|
||||
}
|
||||
priorityFilter = append(priorityFilter, priority)
|
||||
}
|
||||
@@ -201,10 +205,10 @@ func (q *queryFilter) Pass(msg *message) bool {
|
||||
if messagePriority == 0 {
|
||||
messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
|
||||
}
|
||||
if len(q.Priority) > 0 && !util.InIntList(q.Priority, messagePriority) {
|
||||
if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
|
||||
return false
|
||||
}
|
||||
if len(q.Tags) > 0 && !util.InStringListAll(msg.Tags, q.Tags) {
|
||||
if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -2,10 +2,12 @@ package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"golang.org/x/time/rate"
|
||||
"heckel.io/ntfy/util"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,7 +25,7 @@ var (
|
||||
type visitor struct {
|
||||
config *Config
|
||||
messageCache *messageCache
|
||||
ip string
|
||||
ip netip.Addr
|
||||
requests *rate.Limiter
|
||||
emails *rate.Limiter
|
||||
subscriptions util.Limiter
|
||||
@@ -40,7 +42,7 @@ type visitorStats struct {
|
||||
VisitorAttachmentBytesRemaining int64 `json:"visitorAttachmentBytesRemaining"`
|
||||
}
|
||||
|
||||
func newVisitor(conf *Config, messageCache *messageCache, ip string) *visitor {
|
||||
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr) *visitor {
|
||||
return &visitor{
|
||||
config: conf,
|
||||
messageCache: messageCache,
|
||||
@@ -115,7 +117,7 @@ func (v *visitor) Stale() bool {
|
||||
}
|
||||
|
||||
func (v *visitor) Stats() (*visitorStats, error) {
|
||||
attachmentsBytesUsed, err := v.messageCache.AttachmentBytesUsed(v.ip)
|
||||
attachmentsBytesUsed, err := v.messageCache.AttachmentBytesUsed(v.ip.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -11,14 +11,13 @@ import (
|
||||
// CachingEmbedFS is a wrapper around embed.FS that allows setting a ModTime, so that the
|
||||
// default static file server can send 304s back. It can be used like this:
|
||||
//
|
||||
// var (
|
||||
// //go:embed docs
|
||||
// docsStaticFs embed.FS
|
||||
// docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
|
||||
// )
|
||||
//
|
||||
// http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
|
||||
// var (
|
||||
// //go:embed docs
|
||||
// docsStaticFs embed.FS
|
||||
// docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
|
||||
// )
|
||||
//
|
||||
// http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
|
||||
type CachingEmbedFS struct {
|
||||
ModTime time.Time
|
||||
FS embed.FS
|
||||
|
||||
@@ -3,7 +3,6 @@ package util
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -31,8 +30,8 @@ func Gzip(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
var gzPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(ioutil.Discard)
|
||||
New: func() any {
|
||||
w := gzip.NewWriter(io.Discard)
|
||||
return w
|
||||
},
|
||||
}
|
||||
|
||||
47
util/util.go
47
util/util.go
@@ -5,16 +5,18 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"golang.org/x/term"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,8 +37,8 @@ func FileExists(filename string) bool {
|
||||
return stat != nil
|
||||
}
|
||||
|
||||
// InStringList returns true if needle is contained in haystack
|
||||
func InStringList(haystack []string, needle string) bool {
|
||||
// Contains returns true if needle is contained in haystack
|
||||
func Contains[T comparable](haystack []T, needle T) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
@@ -45,8 +47,18 @@ func InStringList(haystack []string, needle string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// InStringListAll returns true if all needles are contained in haystack
|
||||
func InStringListAll(haystack []string, needles []string) bool {
|
||||
// ContainsIP returns true if any one of the of prefixes contains the ip.
|
||||
func ContainsIP(haystack []netip.Prefix, needle netip.Addr) bool {
|
||||
for _, s := range haystack {
|
||||
if s.Contains(needle) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsAll returns true if all needles are contained in haystack
|
||||
func ContainsAll[T comparable](haystack []T, needles []T) bool {
|
||||
matches := 0
|
||||
for _, s := range haystack {
|
||||
for _, needle := range needles {
|
||||
@@ -58,16 +70,6 @@ func InStringListAll(haystack []string, needles []string) bool {
|
||||
return matches == len(needles)
|
||||
}
|
||||
|
||||
// InIntList returns true if needle is contained in haystack
|
||||
func InIntList(haystack []int, needle int) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SplitNoEmpty splits a string using strings.Split, but filters out empty strings
|
||||
func SplitNoEmpty(s string, sep string) []string {
|
||||
res := make([]string, 0)
|
||||
@@ -123,7 +125,8 @@ func ValidRandomString(s string, length int) bool {
|
||||
|
||||
// ParsePriority parses a priority string into its equivalent integer value
|
||||
func ParsePriority(priority string) (int, error) {
|
||||
switch strings.TrimSpace(strings.ToLower(priority)) {
|
||||
p := strings.TrimSpace(strings.ToLower(priority))
|
||||
switch p {
|
||||
case "":
|
||||
return 0, nil
|
||||
case "1", "min":
|
||||
@@ -137,6 +140,11 @@ func ParsePriority(priority string) (int, error) {
|
||||
case "5", "max", "urgent":
|
||||
return 5, nil
|
||||
default:
|
||||
// Ignore new HTTP Priority header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
|
||||
// Cloudflare adds this to requests when forwarding to the backend (ntfy), so we just ignore it.
|
||||
if strings.HasPrefix(p, "u=") {
|
||||
return 3, nil
|
||||
}
|
||||
return 0, errInvalidPriority
|
||||
}
|
||||
}
|
||||
@@ -245,7 +253,7 @@ func BasicAuth(user, pass string) string {
|
||||
|
||||
// MaybeMarshalJSON returns a JSON string of the given object, or "<cannot serialize>" if serialization failed.
|
||||
// This is useful for logging purposes where a failure doesn't matter that much.
|
||||
func MaybeMarshalJSON(v interface{}) string {
|
||||
func MaybeMarshalJSON(v any) string {
|
||||
jsonBytes, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return "<cannot serialize>"
|
||||
@@ -262,7 +270,8 @@ func MaybeMarshalJSON(v interface{}) string {
|
||||
// Warning: Never use this function with the intent to run the resulting command.
|
||||
//
|
||||
// Example:
|
||||
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
|
||||
//
|
||||
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
|
||||
func QuoteCommand(command []string) string {
|
||||
var quoted []string
|
||||
for _, c := range command {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"io/ioutil"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
@@ -19,27 +21,34 @@ func TestRandomString(t *testing.T) {
|
||||
|
||||
func TestFileExists(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "somefile.txt")
|
||||
require.Nil(t, ioutil.WriteFile(filename, []byte{0x25, 0x86}, 0600))
|
||||
require.Nil(t, os.WriteFile(filename, []byte{0x25, 0x86}, 0600))
|
||||
require.True(t, FileExists(filename))
|
||||
require.False(t, FileExists(filename+".doesnotexist"))
|
||||
}
|
||||
|
||||
func TestInStringList(t *testing.T) {
|
||||
s := []string{"one", "two"}
|
||||
require.True(t, InStringList(s, "two"))
|
||||
require.False(t, InStringList(s, "three"))
|
||||
require.True(t, Contains(s, "two"))
|
||||
require.False(t, Contains(s, "three"))
|
||||
}
|
||||
|
||||
func TestInStringListAll(t *testing.T) {
|
||||
s := []string{"one", "two", "three", "four"}
|
||||
require.True(t, InStringListAll(s, []string{"two", "four"}))
|
||||
require.False(t, InStringListAll(s, []string{"three", "five"}))
|
||||
require.True(t, ContainsAll(s, []string{"two", "four"}))
|
||||
require.False(t, ContainsAll(s, []string{"three", "five"}))
|
||||
}
|
||||
|
||||
func TestInIntList(t *testing.T) {
|
||||
func TestContains(t *testing.T) {
|
||||
s := []int{1, 2}
|
||||
require.True(t, InIntList(s, 2))
|
||||
require.False(t, InIntList(s, 3))
|
||||
require.True(t, Contains(s, 2))
|
||||
require.False(t, Contains(s, 3))
|
||||
}
|
||||
|
||||
func TestContainsIP(t *testing.T) {
|
||||
require.True(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("1.1.1.1")))
|
||||
require.True(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("fd12:1234:5678::9876")))
|
||||
require.False(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("1.2.0.1")))
|
||||
require.False(t, ContainsIP([]netip.Prefix{netip.MustParsePrefix("fd00::/8"), netip.MustParsePrefix("1.1.0.0/16")}, netip.MustParseAddr("fc00::1")))
|
||||
}
|
||||
|
||||
func TestSplitNoEmpty(t *testing.T) {
|
||||
@@ -60,13 +69,22 @@ func TestParsePriority(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParsePriority_Invalid(t *testing.T) {
|
||||
priorities := []string{"-1", "6", "aa", "-"}
|
||||
priorities := []string{"-1", "6", "aa", "-", "o=1"}
|
||||
for _, priority := range priorities {
|
||||
_, err := ParsePriority(priority)
|
||||
require.Equal(t, errInvalidPriority, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePriority_HTTPSpecPriority(t *testing.T) {
|
||||
priorities := []string{"u=1", "u=3", "u=7, i"} // see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority
|
||||
for _, priority := range priorities {
|
||||
actual, err := ParsePriority(priority)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, actual) // Always expect 3!
|
||||
}
|
||||
}
|
||||
|
||||
func TestPriorityString(t *testing.T) {
|
||||
priorities := []int{0, 1, 2, 3, 4, 5}
|
||||
expected := []string{"default", "min", "low", "default", "high", "max"}
|
||||
|
||||
7979
web/package-lock.json
generated
7979
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,12 +30,12 @@
|
||||
"prefs_notifications_title": "Известия",
|
||||
"prefs_notifications_sound_title": "Звук при получаване",
|
||||
"prefs_notifications_sound_no_sound": "Без звук",
|
||||
"prefs_notifications_min_priority_title": "Минимален приоритет",
|
||||
"prefs_notifications_min_priority_title": "Най-нисък приоритет",
|
||||
"prefs_notifications_min_priority_any": "Всички",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Нисък приоритет и по-висок",
|
||||
"prefs_notifications_min_priority_default_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_users_add_button": "Добавяне",
|
||||
"prefs_users_dialog_password_label": "Парола",
|
||||
@@ -62,11 +62,11 @@
|
||||
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
|
||||
"notifications_none_for_topic_title": "Липсват известия в темата",
|
||||
"notifications_none_for_any_title": "Липсват известия",
|
||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема, просто направете PUT или POST към адреса ѝ.",
|
||||
"notifications_none_for_any_description": "За да изпратите известия в тема, просто направете PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
||||
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като изпратите съобщения чрез метода PUT или POST ще ги получите тук.",
|
||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса й.",
|
||||
"notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
||||
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.",
|
||||
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
||||
"publish_dialog_priority_min": "Мин. приоритет",
|
||||
"publish_dialog_priority_min": "Най-нисък приоритет",
|
||||
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл",
|
||||
"publish_dialog_base_url_label": "Адрес на услугата",
|
||||
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
|
||||
@@ -78,7 +78,7 @@
|
||||
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
||||
"publish_dialog_tags_label": "Етикети",
|
||||
"publish_dialog_email_label": "Адрес на електронна поща",
|
||||
"publish_dialog_priority_max": "Макс. приоритет",
|
||||
"publish_dialog_priority_max": "Най-висок приоритет",
|
||||
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. warning, srv1-backup",
|
||||
"publish_dialog_click_label": "Адрес",
|
||||
"publish_dialog_topic_label": "Име на темата",
|
||||
@@ -98,7 +98,7 @@
|
||||
"publish_dialog_attached_file_title": "Прикачен файл:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Име на прикачения файл",
|
||||
"publish_dialog_drop_file_here": "Пуснете файла тук",
|
||||
"subscribe_dialog_subscribe_description": "Възможно е темите да не са защитени с парола, затова изберете име, което е трудно за отгатване. След като се абонирате, можете да изпращате известия по PUT или POST.",
|
||||
"subscribe_dialog_subscribe_description": "Възможно е темите да не са защитени с парола, затова изберете име, което е трудно за отгатване. След като се абонирате, можете да изпращате известия чрез методите PUT или POST.",
|
||||
"emoji_picker_search_placeholder": "Търсете емоция",
|
||||
"subscribe_dialog_subscribe_title": "Абониране за тема",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts",
|
||||
@@ -140,10 +140,10 @@
|
||||
"prefs_notifications_sound_description_some": "При пристигане известията са съпроводени от звука „{{sound}}“",
|
||||
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
|
||||
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
|
||||
"priority_min": "минимален",
|
||||
"priority_min": "най-нисък",
|
||||
"priority_low": "нисък",
|
||||
"priority_high": "висок",
|
||||
"priority_max": "максимален",
|
||||
"priority_max": "най-висок",
|
||||
"priority_default": "подразбиран",
|
||||
"prefs_notifications_delete_after_one_week_description": "Известията се премахват автоматично след една седмица",
|
||||
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
|
||||
@@ -160,7 +160,7 @@
|
||||
"nav_button_muted": "Известията са заглушени",
|
||||
"notifications_list": "Списък с известия",
|
||||
"notifications_list_item": "Известие",
|
||||
"notifications_delete": "Изтриване",
|
||||
"notifications_delete": "Премахване",
|
||||
"notifications_mark_read": "Отбелязване като прочетено",
|
||||
"nav_button_connecting": "свързване",
|
||||
"message_bar_show_dialog": "Показване на диалога за публикуване",
|
||||
@@ -169,9 +169,9 @@
|
||||
"notifications_new_indicator": "Ново известие",
|
||||
"notifications_attachment_image": "Прикачено изображение",
|
||||
"notifications_attachment_file_image": "файл на изображение",
|
||||
"notifications_attachment_file_video": "файл на видео",
|
||||
"notifications_attachment_file_audio": "файл на аудио",
|
||||
"notifications_attachment_file_app": "Инсталационен файл на приложение за Android",
|
||||
"notifications_attachment_file_video": "видео",
|
||||
"notifications_attachment_file_audio": "аудио",
|
||||
"notifications_attachment_file_app": "инсталационен файл на приложение за Android",
|
||||
"notifications_attachment_file_document": "друг документ",
|
||||
"publish_dialog_emoji_picker_show": "Избор на емоция",
|
||||
"publish_dialog_topic_reset": "Нулиране на тема",
|
||||
@@ -183,7 +183,7 @@
|
||||
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
|
||||
"prefs_notifications_sound_play": "Възпроизвеждане на избрания звук",
|
||||
"publish_dialog_attach_reset": "Премахване на адреса на файла за прикачане",
|
||||
"prefs_users_delete_button": "Премахване на потребител",
|
||||
"prefs_users_delete_button": "Премахване",
|
||||
"prefs_users_table": "Таблица с потребители",
|
||||
"prefs_users_edit_button": "Промяна на потребител",
|
||||
"error_boundary_unsupported_indexeddb_title": "Поверително разглеждане не се поддържа",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_filename_placeholder": "Dateiname des Anhangs",
|
||||
"publish_dialog_delay_label": "Verzögerung",
|
||||
"publish_dialog_email_placeholder": "Adresse, an die die Benachrichtigung gesendet werden soll, z.B. phil@beispiel.com",
|
||||
"publish_dialog_email_placeholder": "E-Mail-Adresse, an die die Benachrichtigung gesendet werden soll, z.B. phil@example.com",
|
||||
"publish_dialog_chip_click_label": "Klick-URL",
|
||||
"publish_dialog_button_cancel_sending": "Senden abbrechen",
|
||||
"publish_dialog_drop_file_here": "Datei hierher ziehen",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"action_bar_show_menu": "Show menu",
|
||||
"action_bar_logo_alt": "ntfy logo",
|
||||
"action_bar_settings": "Settings",
|
||||
"action_bar_subscription_settings": "Subscription settings",
|
||||
"action_bar_send_test_notification": "Send test notification",
|
||||
"action_bar_clear_notifications": "Clear all notifications",
|
||||
"action_bar_unsubscribe": "Unsubscribe",
|
||||
@@ -59,6 +60,11 @@
|
||||
"notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.",
|
||||
"notifications_example": "Example",
|
||||
"notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.",
|
||||
"subscription_settings_dialog_title": "Subscription settings",
|
||||
"subscription_settings_dialog_description": "Configure settings specifically for this topic subscription. Settings are currently only applied locally.",
|
||||
"subscription_settings_dialog_display_name_placeholder": "Display name",
|
||||
"subscription_settings_button_cancel": "Cancel",
|
||||
"subscription_settings_button_save": "Save",
|
||||
"notifications_loading": "Loading notifications …",
|
||||
"publish_dialog_title_topic": "Publish to {{topic}}",
|
||||
"publish_dialog_title_no_topic": "Publish notification",
|
||||
@@ -123,6 +129,7 @@
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Use another server",
|
||||
"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_subscribe": "Subscribe",
|
||||
"subscribe_dialog_login_title": "Login required",
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"publish_dialog_drop_file_here": "Suelta el archivo aquí",
|
||||
"emoji_picker_search_placeholder": "Buscar emojis",
|
||||
"subscribe_dialog_subscribe_title": "Suscribirse al tópico",
|
||||
"subscribe_dialog_subscribe_description": "Los tópicos pueden no estar protegidos por contraseña, así que elija un nombre que no sea fácil de adivinar. Una vez suscrito, puede hacer PUT/PIST de notificaciones.",
|
||||
"subscribe_dialog_subscribe_description": "Los tópicos pueden no estar protegidos por contraseña, así que elija un nombre que no sea fácil de adivinar. Una vez suscrito, puede hacer PUT/POST de notificaciones.",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Nombre del tópico, ej. phil_alerts",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Usar otro servidor",
|
||||
"subscribe_dialog_login_title": "Es necesario iniciar sesión",
|
||||
|
||||
@@ -152,5 +152,40 @@
|
||||
"error_boundary_stack_trace": "Verem nyomkövetés",
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
|
||||
191
web/public/static/langs/ko.json
Normal file
191
web/public/static/langs/ko.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"action_bar_show_menu": "메뉴 표시",
|
||||
"action_bar_logo_alt": "ntfy 로고",
|
||||
"action_bar_settings": "설정",
|
||||
"action_bar_send_test_notification": "시험용 알림 발송",
|
||||
"action_bar_clear_notifications": "모든 알림 초기화",
|
||||
"action_bar_unsubscribe": "구독 해제",
|
||||
"action_bar_toggle_mute": "알림 음소거/해제",
|
||||
"action_bar_toggle_action_menu": "액션 메뉴 열기/닫기",
|
||||
"message_bar_type_message": "여기에 메세지를 입력하세요",
|
||||
"message_bar_error_publishing": "메세지 발송 오류",
|
||||
"message_bar_show_dialog": "발송 창 표시",
|
||||
"message_bar_publish": "메세지 발송",
|
||||
"nav_topics_title": "구독한 주제",
|
||||
"nav_button_all_notifications": "모든 알림",
|
||||
"nav_button_publish_message": "알림 보내기",
|
||||
"nav_button_subscribe": "주제 구독하기",
|
||||
"nav_button_muted": "알림 음소거됨",
|
||||
"nav_button_connecting": "연결중",
|
||||
"alert_grant_title": "알림이 비활성화되어 있습니다",
|
||||
"alert_grant_description": "데스크톱 알림을 받기 위해서는 브라우저에서 권한을 부여해야 합니다.",
|
||||
"alert_grant_button": "권한 부여하기",
|
||||
"alert_not_supported_title": "알림이 지원되지 않습니다",
|
||||
"notifications_list_item": "알림",
|
||||
"notifications_mark_read": "읽음으로 표시",
|
||||
"notifications_delete": "삭제",
|
||||
"notifications_copied_to_clipboard": "클립보드에 복사됨",
|
||||
"notifications_tags": "태그",
|
||||
"notifications_priority_x": "우선순위 {{priority}}",
|
||||
"notifications_new_indicator": "새 알림",
|
||||
"notifications_attachment_image": "첨부 이미지",
|
||||
"notifications_attachment_copy_url_title": "첨부 주소를 클립보드에 복사",
|
||||
"notifications_attachment_copy_url_button": "URL 복사",
|
||||
"notifications_attachment_open_title": "{{url}}로 가기",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "첨부파일 크기 제한({{fileSizeLimit}}) 초과 및 할당량 초과({{remainingBytes}} 남음)",
|
||||
"publish_dialog_attachment_limits_file_reached": "첨부파일 크기 제한({{fileSizeLimit}}) 초과",
|
||||
"publish_dialog_attachment_limits_quota_reached": "할당량 초과({{remainingBytes}} 남음)",
|
||||
"publish_dialog_emoji_picker_show": "이모지 선택",
|
||||
"publish_dialog_priority_min": "우선순위 최소",
|
||||
"publish_dialog_priority_low": "우선순위 낮음",
|
||||
"publish_dialog_priority_default": "우선순위 기본",
|
||||
"publish_dialog_priority_high": "우선순위 높음",
|
||||
"publish_dialog_priority_max": "우선순위 최상",
|
||||
"publish_dialog_base_url_label": "서비스 URL",
|
||||
"publish_dialog_base_url_placeholder": "서비스 URL, 예를 들면 https://example.com",
|
||||
"publish_dialog_topic_label": "주제 이름",
|
||||
"publish_dialog_topic_placeholder": "주제 이름, 예를 들면 phil_alerts",
|
||||
"publish_dialog_topic_reset": "주제 초기화",
|
||||
"publish_dialog_title_label": "제목",
|
||||
"publish_dialog_title_placeholder": "알림 제목, 예를 들면 디스크 공간 경고",
|
||||
"publish_dialog_message_label": "메세지",
|
||||
"publish_dialog_message_placeholder": "메세지를 여기에 입력하세요",
|
||||
"publish_dialog_tags_label": "태그",
|
||||
"publish_dialog_tags_placeholder": "반점으로 구분된 태그 목록, 예를 들면 warning, srv1-backup",
|
||||
"publish_dialog_priority_label": "우선순위",
|
||||
"publish_dialog_click_label": "클릭 URL",
|
||||
"publish_dialog_click_placeholder": "알림이 클릭되었을때 이동할 URL",
|
||||
"publish_dialog_click_reset": "클릭 URL 제거",
|
||||
"publish_dialog_email_label": "이메일",
|
||||
"publish_dialog_email_placeholder": "알림을 전달할 이메일 주소, 예를 들면 phil@example.com",
|
||||
"publish_dialog_email_reset": "이메일 전달 삭제",
|
||||
"publish_dialog_attach_label": "첨부 파일 URL",
|
||||
"publish_dialog_attach_placeholder": "파일을 URL로 첨부하기, 예를 들면 https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "첨부 파일 URL 삭제",
|
||||
"publish_dialog_filename_label": "파일 이름",
|
||||
"publish_dialog_filename_placeholder": "첨부 파일 이름",
|
||||
"publish_dialog_delay_label": "지연",
|
||||
"publish_dialog_chip_email_label": "이메일로 전달",
|
||||
"publish_dialog_chip_attach_url_label": "URL로 파일 첨부",
|
||||
"publish_dialog_chip_attach_file_label": "로컬 파일 첨부",
|
||||
"publish_dialog_chip_delay_label": "발송 지연",
|
||||
"publish_dialog_chip_topic_label": "주제 변경",
|
||||
"publish_dialog_details_examples_description": "예제와 모든 전송 기능의 자세한 설명은 <docsLink>문서</docsLink>를 참고해주세요.",
|
||||
"publish_dialog_button_cancel": "취소",
|
||||
"publish_dialog_button_send": "보내기",
|
||||
"publish_dialog_button_cancel_sending": "보내기 취소",
|
||||
"publish_dialog_checkbox_publish_another": "다른 메세지 보내기",
|
||||
"publish_dialog_attached_file_title": "첨부된 파일:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "첨부 파일 이름",
|
||||
"publish_dialog_attached_file_remove": "첨부 파일 삭제",
|
||||
"publish_dialog_drop_file_here": "여기에 파일을 끌어다 놓으세요",
|
||||
"emoji_picker_search_placeholder": "이모지 검색",
|
||||
"emoji_picker_search_clear": "검색 초기화",
|
||||
"subscribe_dialog_subscribe_title": "주제 구독하기",
|
||||
"subscribe_dialog_subscribe_description": "주제는 비밀번호로 보호되지 않을 수 있으니 추측하기 어려운 이름을 사용하십시오. 구독한 뒤 PUT/POST 알림을 보낼 수 있습니다.",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "주제 이름, 예를 들면 phil_alerts",
|
||||
"subscribe_dialog_subscribe_use_another_label": "다른 서버 사용",
|
||||
"subscribe_dialog_subscribe_base_url_label": "서비스 URL",
|
||||
"subscribe_dialog_subscribe_button_cancel": "취소",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "구독하기",
|
||||
"subscribe_dialog_login_title": "로그인 필요함",
|
||||
"subscribe_dialog_error_user_anonymous": "익명",
|
||||
"subscribe_dialog_error_user_not_authorized": "사용자 {{username}} 은(는) 인증되지 않았습니다",
|
||||
"subscribe_dialog_login_username_label": "사용자 이름, 예를 들면 phil",
|
||||
"subscribe_dialog_login_password_label": "비밀번호",
|
||||
"subscribe_dialog_login_button_back": "뒤로가기",
|
||||
"subscribe_dialog_login_button_login": "로그인",
|
||||
"prefs_notifications_title": "알림",
|
||||
"prefs_notifications_sound_title": "알림 효과음",
|
||||
"prefs_notifications_sound_description_none": "알림 도착시 효과음을 재생하지 않습니다",
|
||||
"prefs_notifications_sound_description_some": "알림 도착시 {{sound}} 효과음이 재생됩니다",
|
||||
"prefs_notifications_sound_no_sound": "효과음 없음",
|
||||
"prefs_notifications_sound_play": "선택한 효과음 재생",
|
||||
"prefs_notifications_min_priority_title": "우선순위 최소",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "우선순위가 {{number}} ({{name}}) 이상인 알림만 보기",
|
||||
"prefs_notifications_min_priority_description_max": "우선순위가 5 (최상)인 알림만 보기",
|
||||
"prefs_notifications_min_priority_any": "아무 우선순위",
|
||||
"prefs_notifications_min_priority_default_and_higher": "우선순위 기본 이상",
|
||||
"prefs_notifications_min_priority_low_and_higher": "우선순위 낮음 이상",
|
||||
"prefs_notifications_delete_after_three_hours": "3시간 뒤",
|
||||
"prefs_notifications_delete_after_one_day": "1일 뒤",
|
||||
"prefs_notifications_delete_after_one_week": "1주 뒤",
|
||||
"prefs_notifications_delete_after_one_month": "1달 뒤",
|
||||
"prefs_notifications_delete_after_never_description": "알림이 자동으로 삭제되지 않습니다",
|
||||
"prefs_notifications_delete_after_three_hours_description": "알림이 3시간 뒤 자동으로 삭제됩니다",
|
||||
"prefs_notifications_delete_after_one_day_description": "알림이 1일 뒤 자동으로 삭제됩니다",
|
||||
"prefs_notifications_delete_after_one_week_description": "알림이 1주 뒤 자동으로 삭제됩니다",
|
||||
"prefs_notifications_delete_after_one_month_description": "알림이 1달 뒤 자동으로 삭제됩니다",
|
||||
"prefs_users_title": "사용자 관리",
|
||||
"prefs_users_description": "이곳에서 보호된 주제를 위한 사용자를 추가하거나 삭제할 수 있습니다. 사용자 이름과 비밀번호는 브라우저의 로컬 저장소에 보관됩니다.",
|
||||
"prefs_users_add_button": "사용자 추가",
|
||||
"prefs_users_edit_button": "사용자 편집",
|
||||
"prefs_users_delete_button": "사용자 삭제",
|
||||
"prefs_users_table_user_header": "사용자",
|
||||
"prefs_users_table_base_url_header": "서비스 URL",
|
||||
"prefs_users_dialog_title_add": "사용자 추가",
|
||||
"prefs_users_dialog_title_edit": "사용자 편집",
|
||||
"prefs_users_dialog_base_url_label": "서비스 URL, 예를 들면 https://ntfy.sh",
|
||||
"prefs_users_dialog_button_cancel": "취소",
|
||||
"prefs_users_dialog_button_save": "저장",
|
||||
"prefs_appearance_title": "표시 설정",
|
||||
"prefs_users_dialog_button_add": "추가",
|
||||
"prefs_appearance_language_title": "언어",
|
||||
"priority_min": "최하",
|
||||
"priority_low": "낮음",
|
||||
"priority_default": "기본",
|
||||
"priority_high": "높음",
|
||||
"error_boundary_title": "이런, ntfy가 충돌했습니다",
|
||||
"error_boundary_button_copy_stack_trace": "스택 트레이스 복사",
|
||||
"error_boundary_stack_trace": "스택 트레이스",
|
||||
"error_boundary_gathering_info": "더 많은 정보 모으기 …",
|
||||
"error_boundary_unsupported_indexeddb_title": "시크릿 모드는 지원되지 않습니다",
|
||||
"notifications_click_copy_url_button": "링크 복사",
|
||||
"notifications_click_copy_url_title": "링크 URL을 클립보드에 복사",
|
||||
"notifications_attachment_file_video": "동영상 파일",
|
||||
"notifications_attachment_file_app": "안드로이드 앱 파일",
|
||||
"notifications_attachment_file_document": "다른 문서",
|
||||
"notifications_click_open_button": "링크 열기",
|
||||
"notifications_actions_not_supported": "웹앱에서 지원되지 않는 동작입니다",
|
||||
"publish_dialog_title_topic": "{{topic}}에 발송",
|
||||
"alert_not_supported_description": "사용중인 브라우저에서 알림 기능을 지원하지 않습니다.",
|
||||
"notifications_example": "예제",
|
||||
"notifications_more_details": "더 많은 정보가 필요하시다면 <websiteLink>웹사이트</websiteLink>나 <docsLink>문서</docsLink>를 참고하세요.",
|
||||
"notifications_list": "알림 목록",
|
||||
"notifications_attachment_open_button": "첨부 파일 열기",
|
||||
"notifications_no_subscriptions_title": "아직 아무런 구독을 추가하지 않으신 것 같습니다.",
|
||||
"nav_button_settings": "설정",
|
||||
"nav_button_documentation": "문서",
|
||||
"notifications_attachment_link_expires": "링크가 {{date}}에 만료됨",
|
||||
"notifications_attachment_link_expired": "다운로드 링크 만료됨",
|
||||
"notifications_attachment_file_audio": "음성 파일",
|
||||
"notifications_attachment_file_image": "사진 파일",
|
||||
"notifications_actions_open_url_title": "{{url}]로 가기",
|
||||
"notifications_actions_http_request_title": "HTTP {{method}}를 {{url}}에 보내기",
|
||||
"notifications_none_for_topic_title": "아직 이 주제 관련 알림을 받지 않았습니다.",
|
||||
"notifications_none_for_any_title": "아직 어떤 알림도 받지 않았습니다.",
|
||||
"notifications_none_for_any_description": "알림을 받으려면 아래 주소로 PUT이나 POST 요청을 보내세요. 구독중이신 주제 중 하나로 예를 들자면 다음과 같습니다.",
|
||||
"notifications_loading": "알림 불러오는중 …",
|
||||
"publish_dialog_message_published": "알림 발송됨",
|
||||
"notifications_none_for_topic_description": "알림을 받으려면 아래 주소로 PUT이나 POST 요청을 보내세요.",
|
||||
"notifications_no_subscriptions_description": "\"{{linktext}}\" 링크를 눌러서 주제를 생성하거나 구독하세요. 그 다음, 메세지를 PUT이나 POST로 보내면 여기에서 알림을 받으실 수 있습니다.",
|
||||
"publish_dialog_progress_uploading": "업로드중 …",
|
||||
"publish_dialog_title_no_topic": "알림 발송",
|
||||
"publish_dialog_progress_uploading_detail": "업로드중 {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"publish_dialog_delay_placeholder": "알림 발송 지연, 예를 들면 {{unixTimestamp}}, {{relativeTime}} 또는 \"{{naturalLanguage}}\" (영어로 입력)",
|
||||
"publish_dialog_delay_reset": "발송 지연 삭제",
|
||||
"publish_dialog_chip_click_label": "클릭 URL",
|
||||
"subscribe_dialog_login_description": "이 주제는 비밀번호로 보호되어 있습니다. 구독하시려면 사용자 이름과 비밀번호를 입력해주세요.",
|
||||
"prefs_notifications_min_priority_max_only": "우선순위 최상만",
|
||||
"publish_dialog_other_features": "다른 기능:",
|
||||
"prefs_notifications_min_priority_description_any": "우선순위 무관 모든 알림 보기",
|
||||
"prefs_notifications_min_priority_high_and_higher": "우선순위 높음 이상",
|
||||
"error_boundary_unsupported_indexeddb_description": "ntfy 웹 앱은 동작하기 위해서 IndexedDB가 필요하지만 사용중이신 브라우저는 IndexedDB를 시크릿 모드에서 지원하지 않습니다.<br/><br/>안타깝지만 모든 정보는 브라우저에만 저장되므로 ntfy 웹앱을 시크릿 모드에서 사용할 이유는 존재하지 않습니다. <githubLink>이 깃허브 이슈</githubLink>를 참고해 보시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>에서 저희와 이야기를 나눌 수 있습니다.",
|
||||
"prefs_notifications_delete_after_title": "알림 삭제",
|
||||
"prefs_notifications_delete_after_never": "삭제하지 않음",
|
||||
"prefs_users_table": "사용자 테이블",
|
||||
"prefs_users_dialog_username_label": "사용자 이름, 예를 들면 phil",
|
||||
"prefs_users_dialog_password_label": "비밀번호",
|
||||
"priority_max": "최상",
|
||||
"error_boundary_description": "이것은 당연히 발생되어서는 안됩니다. 굉장히 죄송합니다.<br/>가능하시다면 <githubLink>이 문제를 깃허브에 제보</githubLink>해 주시거나, <discordLink>디스코드 서버</discordLink>나 <matrixLink>Matrix</matrixLink>를 통해 알려주세요."
|
||||
}
|
||||
191
web/public/static/langs/pl.json
Normal file
191
web/public/static/langs/pl.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"action_bar_send_test_notification": "Wyślij powiadomienie testowe",
|
||||
"action_bar_clear_notifications": "Wyczyść powiadomienia",
|
||||
"action_bar_toggle_mute": "Włączanie/wyłączanie wyciszania powiadomień",
|
||||
"action_bar_toggle_action_menu": "Otwórz/zamknij menu działań",
|
||||
"message_bar_type_message": "Wpisz wiadomość tutaj",
|
||||
"message_bar_error_publishing": "Błąd przy wysyłaniu powiadomienia",
|
||||
"message_bar_show_dialog": "Pokaż okno dialogowe publikacji",
|
||||
"nav_button_all_notifications": "Wszystkie powiadomienia",
|
||||
"nav_button_documentation": "Dokumentacja",
|
||||
"nav_button_muted": "Powiadomienia wyciszone",
|
||||
"alert_grant_title": "Powiadomienia są wyłączone",
|
||||
"alert_grant_description": "Udziel przeglądarce pozwolenia na wyświetlanie powiadomień na pulpicie.",
|
||||
"alert_grant_button": "Pozwól teraz",
|
||||
"alert_not_supported_title": "Powiadomienia nie są obsługiwane",
|
||||
"alert_not_supported_description": "Powiadomienia nie są obsługiwane przez Twoją przeglądarkę.",
|
||||
"notifications_list": "Lista powiadomień",
|
||||
"notifications_list_item": "Powiadomienie",
|
||||
"notifications_mark_read": "Oznacz jako przeczytane",
|
||||
"notifications_delete": "Usuń",
|
||||
"notifications_copied_to_clipboard": "Skopiowano do schowka",
|
||||
"notifications_tags": "Tagi",
|
||||
"message_bar_publish": "Opublikuj powiadomienie",
|
||||
"nav_topics_title": "Subskrybowane tematy",
|
||||
"nav_button_settings": "Ustawienia",
|
||||
"nav_button_publish_message": "Opublikuj powiadomienie",
|
||||
"nav_button_subscribe": "Zasubskrybuj temat",
|
||||
"nav_button_connecting": "łączenie",
|
||||
"notifications_attachment_image": "Obraz załącznika",
|
||||
"notifications_attachment_copy_url_button": "Kopiuj Adres URL",
|
||||
"notifications_attachment_link_expires": "Łącze wygasa w dniu {{date}}",
|
||||
"notifications_attachment_link_expired": "Łącze do pobrania wygasło",
|
||||
"notifications_attachment_file_image": "plik graficzny",
|
||||
"notifications_attachment_file_video": "plik wideo",
|
||||
"notifications_attachment_file_audio": "plik audio",
|
||||
"notifications_attachment_file_app": "plik aplikacji Android",
|
||||
"notifications_attachment_file_document": "inny dokument",
|
||||
"notifications_click_copy_url_title": "Skopiuj adres URL do schowka",
|
||||
"notifications_click_open_button": "Otwórz łącze",
|
||||
"notifications_actions_open_url_title": "Przejdź do {{url}}",
|
||||
"notifications_actions_not_supported": "Ta akcja nie jest obsługiwana w aplikacji internetowej",
|
||||
"notifications_actions_http_request_title": "Wyślij HTTP {{method}} do {{url}}",
|
||||
"notifications_none_for_topic_title": "Nie otrzymałeś jeszcze żadnych powiadomień dla tego tematu.",
|
||||
"notifications_none_for_any_description": "Aby wysłać powiadomienia do tematu, wyślij PUT/POST do adresu URL tematu. Oto przykład z jednym z twoich tematów.",
|
||||
"notifications_no_subscriptions_title": "Wygląda na to, że nie masz jeszcze żadnych subskrypcji.",
|
||||
"notifications_no_subscriptions_description": "Kliknij łącze \"{{linktext}}\", aby stworzyć lub zasubskrybować temat. Następnie możesz wysyłać wiadomości za pomocą PUT lub POST i otrzymywać powiadomienia tutaj.",
|
||||
"notifications_example": "Przykład",
|
||||
"notifications_loading": "Ładowanie powiadomień …",
|
||||
"publish_dialog_title_topic": "Opublikuj do {{topic}}",
|
||||
"publish_dialog_title_no_topic": "Opublikuj powiadomienie",
|
||||
"publish_dialog_progress_uploading": "Przesyłanie …",
|
||||
"publish_dialog_progress_uploading_detail": "Przesyłanie {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"publish_dialog_message_published": "Powiadomienie wysłane",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "przekracza limit rozmiaru pliku {{fileSizeLimit}}, pozostaje {{remainingBytes}}",
|
||||
"publish_dialog_attachment_limits_file_reached": "przekracza limit rozmiaru pliku {{filesizeLimit}}",
|
||||
"publish_dialog_attachment_limits_quota_reached": "przekracza limit, {{remainingBytes}} pozostało",
|
||||
"publish_dialog_emoji_picker_show": "Wybierz emotkę",
|
||||
"publish_dialog_priority_min": "Min. priorytet",
|
||||
"publish_dialog_priority_low": "Niski priorytet",
|
||||
"publish_dialog_base_url_label": "Adres URL usługi",
|
||||
"publish_dialog_base_url_placeholder": "Adres URL usługi, np. https://example.com",
|
||||
"publish_dialog_topic_label": "Nazwa tematu",
|
||||
"publish_dialog_topic_placeholder": "Nazwa tematu, np. moje_alerty",
|
||||
"publish_dialog_topic_reset": "Resetuj temat",
|
||||
"publish_dialog_title_label": "Tytuł",
|
||||
"publish_dialog_title_placeholder": "Tytuł notyfikacji, np. Niski poziom baterrii",
|
||||
"publish_dialog_message_label": "Wiadomość",
|
||||
"publish_dialog_message_placeholder": "Wpisz wiadomość tutaj",
|
||||
"publish_dialog_tags_label": "Tagi",
|
||||
"publish_dialog_tags_placeholder": "Lista tagów oddzielona przecinkami, np. ostrzeżenie, srv1-backup",
|
||||
"publish_dialog_priority_label": "Priorytet",
|
||||
"publish_dialog_click_label": "Kliknij Adres URL",
|
||||
"publish_dialog_click_placeholder": "Adres URL, który ma być otwarty po kliknięciu na powiadomienie",
|
||||
"publish_dialog_click_reset": "Usuń adres URL kliknięcia",
|
||||
"publish_dialog_email_label": "Email",
|
||||
"publish_dialog_email_placeholder": "Adres, na który ma być wysłane powiadomienie, np. phil@example.com",
|
||||
"publish_dialog_email_reset": "Usuń przekazywanie wiadomości email",
|
||||
"publish_dialog_attach_label": "Adres URL załącznika",
|
||||
"publish_dialog_attach_placeholder": "Dołączenie pliku z adresu URL, np. https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "Usuń adres URL załącznika",
|
||||
"publish_dialog_filename_label": "Nazwa pliku",
|
||||
"publish_dialog_filename_placeholder": "Nazwa pliku załącznika",
|
||||
"publish_dialog_delay_label": "Opóźnienie",
|
||||
"publish_dialog_delay_reset": "Usuń opóźnione dostarczenie",
|
||||
"publish_dialog_other_features": "Inne funkcje:",
|
||||
"publish_dialog_chip_click_label": "Adres URL kliknięcia",
|
||||
"publish_dialog_chip_email_label": "Przekaż na email",
|
||||
"publish_dialog_chip_attach_url_label": "Dołącz plik z adresu URL",
|
||||
"publish_dialog_chip_attach_file_label": "Dołącz plik lokalny",
|
||||
"publish_dialog_chip_delay_label": "Opóźnienie dostawy",
|
||||
"publish_dialog_chip_topic_label": "Zmień temat",
|
||||
"publish_dialog_details_examples_description": "Przykłady i szczegółowe informacje na temat wszystkich opcji można znaleźć w <docsLink>dokumentacji</docsLink>.",
|
||||
"publish_dialog_button_cancel_sending": "Anuluj wysyłanie",
|
||||
"publish_dialog_button_send": "Wyślij",
|
||||
"publish_dialog_checkbox_publish_another": "Wyślij kolejną wiadomość",
|
||||
"publish_dialog_attached_file_title": "Załączony plik:",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Nazwa pliku załącznika",
|
||||
"publish_dialog_drop_file_here": "Upuść plik tutaj",
|
||||
"emoji_picker_search_placeholder": "Szukaj emotki",
|
||||
"emoji_picker_search_clear": "Wyczyść wyszukiwanie",
|
||||
"subscribe_dialog_subscribe_title": "Zasubskrybuj temat",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Nazwa tematu, np. moje_alerty",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Użyj innego serwera",
|
||||
"subscribe_dialog_subscribe_base_url_label": "Adres URL usługi",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Anuluj",
|
||||
"subscribe_dialog_login_description": "Ten temat jest chroniony hasłem. Proszę podać nazwę użytkownika i hasło, aby zasubskrybować.",
|
||||
"subscribe_dialog_login_username_label": "Nazwa użytkownika, np. phil",
|
||||
"subscribe_dialog_login_password_label": "Hasło",
|
||||
"publish_dialog_button_cancel": "Anuluj",
|
||||
"subscribe_dialog_login_button_back": "Powrót",
|
||||
"subscribe_dialog_login_button_login": "Zaloguj się",
|
||||
"subscribe_dialog_error_user_not_authorized": "Użytkownik {{username}} nie ma uprawnień",
|
||||
"subscribe_dialog_error_user_anonymous": "anonim",
|
||||
"prefs_notifications_title": "Powiadomienia",
|
||||
"prefs_notifications_sound_title": "Dźwięk powiadomienia",
|
||||
"prefs_notifications_sound_description_none": "Brak dźwięku po otrzymaniu powiadomienia",
|
||||
"prefs_notifications_sound_description_some": "Odtwarzaj dźwięk {{sound}}, gdy nadejdzie powiadomienie",
|
||||
"prefs_notifications_sound_play": "Odtwórz wybrany dźwięk",
|
||||
"prefs_notifications_min_priority_title": "Minimalny priorytet",
|
||||
"prefs_notifications_min_priority_description_any": "Pokaż wszystkie powiadomienia, niezależnie od priorytetu",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "Pokazuj powiadomienia, gdy ich priorytet to {{number}} ({{name}}) lub wyższy",
|
||||
"prefs_notifications_min_priority_description_max": "Pokaż powiadomienia, jeśli priorytet wynosi 5 (max)",
|
||||
"prefs_notifications_min_priority_any": "Dowolny priorytet",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Niski priorytet i wyższy",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Priorytet standardowy i wyższy",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Wysoki priorytet i wyższy",
|
||||
"prefs_notifications_delete_after_one_day": "Po jednym dniu",
|
||||
"prefs_notifications_delete_after_one_week": "Po tygodniu",
|
||||
"prefs_notifications_delete_after_one_month": "Po miesiącu",
|
||||
"prefs_notifications_delete_after_never_description": "Powiadomienia nigdy nie są automatycznie usuwane",
|
||||
"prefs_notifications_delete_after_three_hours_description": "Powiadomienia są automatycznie usuwane po trzech godzinach",
|
||||
"prefs_notifications_delete_after_one_day_description": "Powiadomienia są automatycznie usuwane po jednym dniu",
|
||||
"prefs_notifications_delete_after_one_month_description": "Powiadomienia są automatycznie usuwane po upływie jednego miesiąca",
|
||||
"prefs_notifications_delete_after_one_week_description": "Powiadomienia są automatycznie usuwane po upływie jedego tygodnia",
|
||||
"prefs_users_title": "Zarządzaj użytkownikami",
|
||||
"prefs_users_description": "Dodaj/usuń użytkowników dla tematów chronionych hasłem. Uwaga: Nazwa użytkownika i hasło są przechowywane w lokalnej pamięci przeglądarki.",
|
||||
"prefs_users_table": "Tabela użytkowników",
|
||||
"prefs_users_add_button": "Dodaj użytkownika",
|
||||
"notifications_attachment_open_button": "Otwórz załącznik",
|
||||
"prefs_users_edit_button": "Edytuj użytkownika",
|
||||
"prefs_users_delete_button": "Usuń użytkownika",
|
||||
"prefs_users_table_base_url_header": "Adres URL usługi",
|
||||
"prefs_users_dialog_title_add": "Dodaj użytkownika",
|
||||
"prefs_users_dialog_button_cancel": "Anuluj",
|
||||
"prefs_users_dialog_button_add": "Dodaj",
|
||||
"prefs_users_dialog_button_save": "Zapisz",
|
||||
"prefs_appearance_title": "Wygląd",
|
||||
"prefs_appearance_language_title": "Język",
|
||||
"error_boundary_title": "Oh nie, ntfy przestało działać",
|
||||
"error_boundary_description": "Oczywiście, to nie miało się wydarzyć. Bardzo przepraszam za to.<br/>Jeśli masz minutę, proszę <githubLink>zgłoś to na GitHubie</githubLink>, albo daj nam znać przez <discordLink>Discord</discordLink> lub <matrixLink>Matrix</matrixLink>.",
|
||||
"error_boundary_button_copy_stack_trace": "Kopiuj stack trace",
|
||||
"error_boundary_stack_trace": "Stack trace",
|
||||
"error_boundary_gathering_info": "Zbierz więcej informacji …",
|
||||
"error_boundary_unsupported_indexeddb_title": "Prywatne karty przeglądarki nie są obsługiwane",
|
||||
"action_bar_show_menu": "Pokaż menu",
|
||||
"action_bar_logo_alt": "ntfy logo",
|
||||
"action_bar_unsubscribe": "Zrezygnuj z subskrypcji",
|
||||
"notifications_attachment_copy_url_title": "Kopiuj adres URL załącznika do schowka",
|
||||
"action_bar_settings": "Ustawienia",
|
||||
"notifications_priority_x": "Priorytet {{priority}}",
|
||||
"notifications_new_indicator": "Nowe powiadomienie",
|
||||
"notifications_attachment_open_title": "Przejdź do {{url}}",
|
||||
"notifications_click_copy_url_button": "Skopiuj łącze",
|
||||
"notifications_none_for_topic_description": "Aby wysłać powiadomienia do tego tematu, wyślij PUT lub POST-Request na adres URL tematu.",
|
||||
"notifications_none_for_any_title": "Nie otrzymałeś żadnych powiadomień.",
|
||||
"notifications_more_details": "Bardziej szczegółowe informacje można znaleźć na <websiteLink>stronie internetowej</websiteLink> oraz w <docsLink>dokumentacji</docsLink>.",
|
||||
"publish_dialog_priority_default": "Domyślny priorytet",
|
||||
"publish_dialog_priority_max": "Max. priorytet",
|
||||
"publish_dialog_priority_high": "Wysoki priorytet",
|
||||
"publish_dialog_delay_placeholder": "Opóźnienie dostarczenie, np.{{unixTimestamp}}, {{relativeTime}}, lub \"{{naturalLanguage}}\" (tylko w języku angielskim)",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "Subskrybuj",
|
||||
"prefs_users_table_user_header": "Użytkownik",
|
||||
"publish_dialog_attached_file_remove": "Usuń załączony plik",
|
||||
"subscribe_dialog_subscribe_description": "Tematy nie mogą być chronione hasłem, więc wybierz trudną do odgadnięcia nazwę. Po zasubskrybowaniu możesz wysyłać powiadomienia poprzez POST/PUT.",
|
||||
"subscribe_dialog_login_title": "Wymagane jest zalogowanie się",
|
||||
"prefs_notifications_delete_after_title": "Usuń powiadomienia",
|
||||
"prefs_users_dialog_password_label": "Hasło",
|
||||
"priority_low": "niski",
|
||||
"priority_default": "podstawowy",
|
||||
"priority_max": "maksymalny",
|
||||
"prefs_notifications_delete_after_three_hours": "Po trzech godzinach",
|
||||
"prefs_users_dialog_base_url_label": "Adres URL usługi, np. https://ntfy.sh",
|
||||
"prefs_notifications_sound_no_sound": "Bez dzwięku",
|
||||
"prefs_users_dialog_username_label": "Nazwa użytkownika, np. phil",
|
||||
"priority_high": "wysoki",
|
||||
"prefs_notifications_min_priority_max_only": "Tylko maksymalny priorytet",
|
||||
"prefs_notifications_delete_after_never": "Nigdy",
|
||||
"prefs_users_dialog_title_edit": "Edytuj użytkownika",
|
||||
"priority_min": "minimum",
|
||||
"error_boundary_unsupported_indexeddb_description": "Aplikacja ntfy potrzebuje IndexedDB, aby działać poprawnie, a Twoja przeglądarka nie obsługuje IndexedDB w prywatnych zakładkach.<br/><br/>To denerwujące, ale używanie ntfy w prywatnej zakładce nie ma sensu, ponieważ wszystkie dane są przechowywane w przeglądarce. Więcej informacji można uzyskać <githubLink>w tym wydaniu GitHub</githubLink>, lub na czacie w <discordLink>Discord</discordLink> lub <matrixLink>Matrix</matrixLink>."
|
||||
}
|
||||
1
web/public/static/langs/pt.json
Normal file
1
web/public/static/langs/pt.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -150,5 +150,9 @@
|
||||
"error_boundary_stack_trace": "Трассировка стека",
|
||||
"error_boundary_gathering_info": "Соберите больше информации …",
|
||||
"publish_dialog_drop_file_here": "Перетащите файл юда",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Высокий приоритет и выше"
|
||||
"prefs_notifications_min_priority_high_and_higher": "Высокий приоритет и выше",
|
||||
"action_bar_toggle_action_menu": "Открыть/закрыть меню",
|
||||
"action_bar_show_menu": "Показать меню",
|
||||
"action_bar_logo_alt": "ntfy лого",
|
||||
"emoji_picker_search_clear": "Очистить поиск"
|
||||
}
|
||||
|
||||
51
web/public/static/langs/sv.json
Normal file
51
web/public/static/langs/sv.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"action_bar_settings": "Inställningar",
|
||||
"action_bar_send_test_notification": "Skicka test notis",
|
||||
"action_bar_toggle_action_menu": "Öppna/stäng åtgärdsmeny",
|
||||
"message_bar_type_message": "Skriv ett meddelande här",
|
||||
"message_bar_error_publishing": "Fel vid publicering av notis",
|
||||
"message_bar_show_dialog": "Visa publicerings dialog",
|
||||
"message_bar_publish": "Publicera meddelande",
|
||||
"nav_topics_title": "Prenumererade kategorier",
|
||||
"nav_button_all_notifications": "Alla notiser",
|
||||
"nav_button_documentation": "Dokumentation",
|
||||
"nav_button_publish_message": "Publicera notis",
|
||||
"nav_button_subscribe": "Prenumerera på kategori",
|
||||
"alert_grant_title": "Notiser är avstängda",
|
||||
"alert_grant_button": "Bevilja nu",
|
||||
"alert_not_supported_title": "Notiser stöds inte",
|
||||
"notifications_list": "Notis-lista",
|
||||
"notifications_list_item": "Notis",
|
||||
"notifications_delete": "Radera",
|
||||
"notifications_copied_to_clipboard": "Kopierat till urklipp",
|
||||
"notifications_tags": "Taggar",
|
||||
"notifications_new_indicator": "Ny notis",
|
||||
"notifications_attachment_copy_url_title": "Kopiera bifogad URL till urklipp",
|
||||
"notifications_attachment_copy_url_button": "Kopiera URL",
|
||||
"notifications_attachment_open_title": "Gå till {{url}}",
|
||||
"notifications_attachment_open_button": "Öppna bilagan",
|
||||
"notifications_attachment_link_expired": "Nedladdningslänk utgått",
|
||||
"notifications_priority_x": "Prioritet {{priority}}",
|
||||
"action_bar_show_menu": "Visa meny",
|
||||
"action_bar_logo_alt": "ntfy logga",
|
||||
"action_bar_unsubscribe": "Avprenumerera",
|
||||
"action_bar_toggle_mute": "Tysta/aktivera notiser",
|
||||
"action_bar_clear_notifications": "Rensa alla notiser",
|
||||
"nav_button_connecting": "ansluter",
|
||||
"notifications_attachment_image": "Bifogad bild",
|
||||
"nav_button_settings": "Inställningar",
|
||||
"nav_button_muted": "Notiser tystade",
|
||||
"notifications_attachment_link_expires": "länken utgår {{date}}",
|
||||
"notifications_attachment_file_image": "bild fil",
|
||||
"notifications_attachment_file_audio": "ljud fil",
|
||||
"alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.",
|
||||
"alert_not_supported_description": "Notiser stöds inte i din webbläsare.",
|
||||
"notifications_mark_read": "Markera som läst",
|
||||
"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 …"
|
||||
}
|
||||
191
web/public/static/langs/uk.json
Normal file
191
web/public/static/langs/uk.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"action_bar_logo_alt": "логотип ntfy",
|
||||
"action_bar_settings": "Налаштування",
|
||||
"message_bar_type_message": "Введіть повідомлення тут",
|
||||
"message_bar_error_publishing": "Помилка публікації сповіщення",
|
||||
"message_bar_show_dialog": "Показати діалогове вікно публікації",
|
||||
"nav_topics_title": "Підписки на теми",
|
||||
"nav_button_settings": "Налаштування",
|
||||
"nav_button_documentation": "Документація",
|
||||
"nav_button_subscribe": "Підписатися на тему",
|
||||
"nav_button_muted": "Сповіщення вимкнено",
|
||||
"nav_button_connecting": "підключення",
|
||||
"alert_grant_title": "Сповіщення вимкнено",
|
||||
"alert_grant_description": "Дозвольте браузеру показувати сповіщення.",
|
||||
"alert_grant_button": "Дозволити",
|
||||
"alert_not_supported_title": "Сповіщення не підтримуються",
|
||||
"notifications_list_item": "Сповіщення",
|
||||
"notifications_attachment_image": "Прикріплене зображення",
|
||||
"notifications_attachment_open_title": "Перейти на {{url}}",
|
||||
"notifications_attachment_open_button": "Відкрити вкладення",
|
||||
"notifications_attachment_link_expires": "термін дії посилання закінчується {{date}}",
|
||||
"notifications_actions_http_request_title": "Надіслати HTTP {{method}} на {{url}}",
|
||||
"notifications_none_for_any_title": "Ви не отримали жодних сповіщень.",
|
||||
"notifications_no_subscriptions_description": "Натисніть \"{{linktext}}\" посилання, щоб створити або підписатися на тему. Після цього ви зможете надсилати повідомлення за допомогою PUT або POST, і ви отримуватимете тут повідомлення.",
|
||||
"notifications_more_details": "Додаткову інформацію можна знайти на <websiteLink>сайті</websiteLink> або в <docsLink>документації</docsLink>.",
|
||||
"notifications_loading": "Завантаження сповіщень…",
|
||||
"publish_dialog_title_topic": "Опублікувати в {{topic}}",
|
||||
"publish_dialog_title_no_topic": "Опублікувати сповіщення",
|
||||
"publish_dialog_progress_uploading": "Завантаження…",
|
||||
"publish_dialog_message_published": "Сповіщення опубліковано",
|
||||
"publish_dialog_attachment_limits_quota_reached": "перевищує квоту, залишилося {{remainingBytes}}",
|
||||
"publish_dialog_priority_low": "Низький пріоритет",
|
||||
"publish_dialog_topic_label": "Назва теми",
|
||||
"publish_dialog_topic_placeholder": "Назва теми, наприклад phil_alerts",
|
||||
"publish_dialog_topic_reset": "Скинути тему",
|
||||
"publish_dialog_title_label": "Заголовок",
|
||||
"publish_dialog_title_placeholder": "Заголовок сповіщення, наприклад Сповіщення про дисковий простір",
|
||||
"publish_dialog_message_label": "Повідомлення",
|
||||
"publish_dialog_message_placeholder": "Введіть повідомлення",
|
||||
"publish_dialog_tags_label": "Теги",
|
||||
"publish_dialog_tags_placeholder": "Список тегів розділений комою, наприклад warning, srv1-backup",
|
||||
"publish_dialog_click_placeholder": "URL-адреса, яка відкривається після натискання сповіщення",
|
||||
"publish_dialog_email_label": "Електронна пошта",
|
||||
"publish_dialog_attach_placeholder": "Прикріпіть файл за URL-адресою, наприклад https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "Видалити URL вкладення",
|
||||
"publish_dialog_filename_placeholder": "Ім'я файлу вкладення",
|
||||
"publish_dialog_delay_reset": "Видалити затримку доставлення",
|
||||
"publish_dialog_chip_click_label": "Адреса",
|
||||
"publish_dialog_chip_email_label": "Переслати на електронну пошту",
|
||||
"publish_dialog_chip_topic_label": "Змінити тему",
|
||||
"publish_dialog_attached_file_remove": "Видалити прикріплений файл",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Назва теми, наприклад phil_alerts",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Використовувати інший сервер",
|
||||
"subscribe_dialog_subscribe_base_url_label": "URL служби",
|
||||
"subscribe_dialog_login_password_label": "Пароль",
|
||||
"subscribe_dialog_login_button_back": "Назад",
|
||||
"subscribe_dialog_error_user_not_authorized": "{{username}} користувач не авторизований",
|
||||
"prefs_notifications_sound_description_none": "Сповіщення не відтворюють жодного звуку при надходженні",
|
||||
"prefs_notifications_sound_description_some": "Сповіщення відтворюють звук {{sound}}",
|
||||
"prefs_notifications_min_priority_description_any": "Показати всі сповіщень, незалежно від пріоритету",
|
||||
"prefs_notifications_min_priority_any": "Будь-який пріоритет",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Пріоритет за замовчуванням та високий",
|
||||
"prefs_notifications_delete_after_title": "Видалити сповіщення",
|
||||
"prefs_notifications_delete_after_never": "Ніколи",
|
||||
"prefs_notifications_delete_after_one_day": "Через день",
|
||||
"prefs_notifications_delete_after_one_week": "Через тиждень",
|
||||
"prefs_notifications_delete_after_one_month": "Через місяць",
|
||||
"prefs_notifications_delete_after_never_description": "Сповіщення ніколи не видаляються автоматично",
|
||||
"prefs_notifications_delete_after_three_hours_description": "Сповіщення автоматично видаляються через три години",
|
||||
"prefs_notifications_delete_after_one_day_description": "Сповіщення автоматично видаляються через один день",
|
||||
"prefs_notifications_delete_after_one_week_description": "Сповіщення автоматично видаляються через тиждень",
|
||||
"prefs_notifications_delete_after_one_month_description": "Сповіщення автоматично видаляються через місяць",
|
||||
"prefs_users_title": "Керувати користувачами",
|
||||
"prefs_users_table": "Таблиця користувачів",
|
||||
"prefs_users_edit_button": "Редагувати користувача",
|
||||
"prefs_users_dialog_button_save": "Зберегти",
|
||||
"prefs_appearance_title": "Зовнішній вигляд",
|
||||
"priority_default": "за замовчуванням",
|
||||
"priority_high": "високий",
|
||||
"priority_max": "макс",
|
||||
"error_boundary_title": "Ой, ntfy впав",
|
||||
"error_boundary_button_copy_stack_trace": "Копіювати трасування стека",
|
||||
"action_bar_show_menu": "Показати меню",
|
||||
"action_bar_toggle_action_menu": "Відкрити/закрити меню",
|
||||
"action_bar_send_test_notification": "Надіслати тестове сповіщення",
|
||||
"action_bar_clear_notifications": "Очистити всі сповіщення",
|
||||
"action_bar_toggle_mute": "Вимкнути/увімкнути сповіщення",
|
||||
"action_bar_unsubscribe": "Відписатися",
|
||||
"message_bar_publish": "Опублікувати повідомлення",
|
||||
"nav_button_all_notifications": "Усі сповіщення",
|
||||
"alert_not_supported_description": "Ваш браузер не підтримує сповіщення.",
|
||||
"notifications_list": "Список сповіщень",
|
||||
"notifications_mark_read": "Позначити як прочитане",
|
||||
"notifications_delete": "Видалити",
|
||||
"notifications_tags": "Теги",
|
||||
"nav_button_publish_message": "Опублікувати сповіщення",
|
||||
"notifications_attachment_copy_url_title": "Копіювати URL-адресу вкладення",
|
||||
"notifications_attachment_link_expired": "термін дії посилання для завантаження закінчився",
|
||||
"publish_dialog_progress_uploading_detail": "Завантажується {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"notifications_priority_x": "Пріоритет {{priority}}",
|
||||
"notifications_attachment_copy_url_button": "Копіювати URL-адресу",
|
||||
"notifications_copied_to_clipboard": "Скопійовано в буфер обміну",
|
||||
"notifications_attachment_file_video": "відео файл",
|
||||
"notifications_attachment_file_audio": "звуковий файл",
|
||||
"publish_dialog_emoji_picker_show": "Виберіть емодзі",
|
||||
"notifications_new_indicator": "Нове сповіщення",
|
||||
"notifications_attachment_file_image": "файл зображення",
|
||||
"notifications_attachment_file_document": "інший документ",
|
||||
"notifications_click_copy_url_title": "Копіювати URL-адресу посилання",
|
||||
"notifications_click_copy_url_button": "Копіювати посилання",
|
||||
"notifications_actions_not_supported": "Дія не підтримується у браузері",
|
||||
"notifications_attachment_file_app": "Файл програми Android",
|
||||
"notifications_click_open_button": "Відкрити посилання",
|
||||
"notifications_actions_open_url_title": "Перейти на {{url}}",
|
||||
"notifications_none_for_topic_description": "Щоб надіслати сповіщення до цієї теми, просто надішліть PUT або POST на URL-адресу цієї теми.",
|
||||
"notifications_no_subscriptions_title": "Схоже, у вас ще немає жодної підписки.",
|
||||
"publish_dialog_drop_file_here": "Перетягніть файл сюди",
|
||||
"notifications_none_for_topic_title": "Ви ще не отримували сповіщення на цю тему.",
|
||||
"notifications_example": "Приклад",
|
||||
"notifications_none_for_any_description": "Щоб надіслати сповіщення до теми, просто надішліть PUT або POST на URL-адресу теми. Ось приклад, використовуючи одну з ваших тем.",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "перевищує {{fileSizeLimit}} розмір файлу, {{remainingBytes}} залишилося",
|
||||
"publish_dialog_priority_default": "Пріоритет за замовчуванням",
|
||||
"publish_dialog_attachment_limits_file_reached": "перевищує {{fileSizeLimit}} розмір файлу",
|
||||
"publish_dialog_priority_min": "Мін. пріоритет",
|
||||
"publish_dialog_priority_high": "Високий пріоритет",
|
||||
"publish_dialog_priority_max": "Макс. пріоритет",
|
||||
"publish_dialog_base_url_placeholder": "URL-адреса сервісу, наприклад https://example.com",
|
||||
"publish_dialog_base_url_label": "URL служби",
|
||||
"publish_dialog_other_features": "Інші можливості:",
|
||||
"publish_dialog_chip_attach_file_label": "Прикріпити локальний файл",
|
||||
"publish_dialog_priority_label": "Пріоритет",
|
||||
"publish_dialog_click_label": "Натисніть URL",
|
||||
"publish_dialog_click_reset": "Видалити URL-адресу для натискання",
|
||||
"publish_dialog_email_placeholder": "Адреса для пересилання сповіщення, наприклад phil@example.com",
|
||||
"publish_dialog_attach_label": "URL-адреса вкладення",
|
||||
"publish_dialog_filename_label": "Ім'я файлу",
|
||||
"publish_dialog_delay_label": "Затримка",
|
||||
"publish_dialog_email_reset": "Видалити пересилання електронної пошти",
|
||||
"publish_dialog_chip_attach_url_label": "Прикріпити файл за URL",
|
||||
"publish_dialog_details_examples_description": "Приклади та докладний опис усіх функцій, зверніться до <docsLink>документації</docsLink>.",
|
||||
"publish_dialog_button_cancel_sending": "Скасувати відправку",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Ім'я прикріпленого файлу",
|
||||
"publish_dialog_delay_placeholder": "Затримка доставлення, наприклад {{unixTimestamp}}, {{relativeTime}} або \"{{naturalLanguage}}\" (лише англійською)",
|
||||
"publish_dialog_button_send": "Надіслати",
|
||||
"publish_dialog_checkbox_publish_another": "Опублікувати ще",
|
||||
"publish_dialog_chip_delay_label": "Затримка доставлення",
|
||||
"publish_dialog_button_cancel": "Скасувати",
|
||||
"publish_dialog_attached_file_title": "Прикріплений файл:",
|
||||
"subscribe_dialog_subscribe_description": "Теми можуть не бути захищені паролем, тому виберіть назву, яку нелегко вгадати. Після підписки ви можете PUT/POST сповіщення.",
|
||||
"emoji_picker_search_placeholder": "Пошук емодзі",
|
||||
"emoji_picker_search_clear": "Очистити пошук",
|
||||
"subscribe_dialog_subscribe_title": "Підпишіться на тему",
|
||||
"subscribe_dialog_login_username_label": "Ім'я користувача, наприклад phil",
|
||||
"prefs_notifications_title": "Сповіщення",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Скасувати",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "Підписатися",
|
||||
"subscribe_dialog_error_user_anonymous": "анонімний",
|
||||
"subscribe_dialog_login_title": "Потрібна авторизація",
|
||||
"subscribe_dialog_login_description": "Ця тема захищена паролем. Будь ласка, введіть ім'я користувача та пароль, щоб підписатися.",
|
||||
"prefs_notifications_sound_title": "Звук сповіщення",
|
||||
"subscribe_dialog_login_button_login": "Логін",
|
||||
"prefs_notifications_sound_no_sound": "Без звука",
|
||||
"prefs_notifications_sound_play": "Відтворення вибраного звуку",
|
||||
"prefs_users_description": "Додайте/видаляйте користувачів для захищених тем. Зверніть увагу, що ім'я користувача та пароль зберігаються у локальному сховищі браузера.",
|
||||
"prefs_notifications_min_priority_title": "Мінімальний пріоритет",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Високий пріоритет і вище",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "Показувати сповіщення, якщо пріоритет {{number}} ({{name}}) або вище",
|
||||
"prefs_notifications_min_priority_description_max": "Показувати сповіщення, якщо пріоритет 5 (макс.)",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Низький та високий пріоритет",
|
||||
"prefs_notifications_min_priority_max_only": "Тільки максимальний пріоритет",
|
||||
"prefs_users_table_base_url_header": "URL служби",
|
||||
"prefs_users_dialog_password_label": "Пароль",
|
||||
"prefs_notifications_delete_after_three_hours": "Через три години",
|
||||
"prefs_users_add_button": "Додати користувача",
|
||||
"prefs_users_dialog_title_edit": "Редагувати користувача",
|
||||
"prefs_users_dialog_base_url_label": "URL-адреса служби, наприклад https://ntfy.sh",
|
||||
"prefs_users_delete_button": "Видалити користувача",
|
||||
"prefs_users_table_user_header": "Користувач",
|
||||
"prefs_users_dialog_title_add": "Додати користувача",
|
||||
"prefs_users_dialog_username_label": "Ім'я користувача, наприклад phil",
|
||||
"prefs_users_dialog_button_cancel": "Скасувати",
|
||||
"prefs_users_dialog_button_add": "Додати",
|
||||
"prefs_appearance_language_title": "Мова",
|
||||
"error_boundary_gathering_info": "Зберіть більше інформації…",
|
||||
"priority_min": "мін",
|
||||
"error_boundary_description": "Очевидно, цього не повинно статися. Дуже шкода.<br/>Якщо у вас є хвилина, <githubLink>повідомте про це на GitHub</githubLink> або повідомте нам через <discordLink>Discord</discordLink> або <matrixLink>Matrix</matrixLink> .",
|
||||
"priority_low": "низький",
|
||||
"error_boundary_stack_trace": "Трасування стека",
|
||||
"error_boundary_unsupported_indexeddb_title": "Приватний перегляд не підтримується",
|
||||
"error_boundary_unsupported_indexeddb_description": "Веб-програма ntfy потребує IndexedDB для роботи, а ваш браузер не підтримує IndexedDB у режимі приватного перегляду.<br/><br/>На жаль, використання ntfy web не має сенсу у режимі приватного перегляду, оскільки все зберігається в пам’яті браузера. Ви можете прочитати більше про це <githubLink>у цьому випуску GitHub</githubLink> або поспілкуватися з нами на <discordLink>Discord</discordLink> або <matrixLink>Matrix</matrixLink>."
|
||||
}
|
||||
@@ -118,8 +118,8 @@
|
||||
"prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知",
|
||||
"prefs_notifications_min_priority_any": "任意优先级",
|
||||
"prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_default_and_higher": "默认优先级或更高优先级",
|
||||
"prefs_notifications_min_priority_high_and_higher": "高优先级或更高优先级",
|
||||
"prefs_notifications_min_priority_default_and_higher": "默认优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_high_and_higher": "高优先级和更高优先级",
|
||||
"prefs_notifications_min_priority_max_only": "仅最高优先级",
|
||||
"prefs_notifications_delete_after_never": "从不",
|
||||
"prefs_notifications_delete_after_one_month": "一月后",
|
||||
@@ -186,6 +186,6 @@
|
||||
"prefs_users_edit_button": "编辑用户",
|
||||
"publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup",
|
||||
"publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅<docsLink>文档</docsLink>。",
|
||||
"subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易猜测的名字。订阅后,您可以使用 PUT/POST 通知。",
|
||||
"subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易被猜中的名字。订阅后,您可以使用 PUT/POST 通知。",
|
||||
"publish_dialog_delay_placeholder": "延期投递,例如 {{unixTimestamp}}、{{relativeTime}}或「{{naturalLanguage}}」(仅限英语)"
|
||||
}
|
||||
|
||||
85
web/public/static/langs/zh_Hant.json
Normal file
85
web/public/static/langs/zh_Hant.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"action_bar_logo_alt": "ntfy 標識",
|
||||
"action_bar_unsubscribe": "取消訂閱",
|
||||
"action_bar_toggle_mute": "通知靜音/解除通知靜音",
|
||||
"action_bar_toggle_action_menu": "開啟/關閉操作選單",
|
||||
"message_bar_type_message": "在這邊輸入訊息",
|
||||
"alert_grant_description": "允許瀏覽器權限以顯示桌面通知。",
|
||||
"alert_grant_button": "允許",
|
||||
"notifications_list": "通知清單",
|
||||
"notifications_list_item": "通知",
|
||||
"notifications_mark_read": "標示已讀",
|
||||
"notifications_attachment_image": "附加圖片",
|
||||
"notifications_attachment_copy_url_title": "複製附件 URL 到剪貼簿",
|
||||
"notifications_attachment_copy_url_button": "複製 URL",
|
||||
"notifications_attachment_open_title": "前往 {{url}}",
|
||||
"notifications_attachment_open_button": "開啟附件",
|
||||
"notifications_attachment_link_expired": "下載連結已過期",
|
||||
"notifications_attachment_file_video": "影片檔案",
|
||||
"notifications_attachment_file_app": "Android 應用程式檔案",
|
||||
"notifications_attachment_file_document": "其他文件",
|
||||
"notifications_click_copy_url_title": "複製連結 URL 到剪貼板",
|
||||
"notifications_click_copy_url_button": "複製連結",
|
||||
"notifications_click_open_button": "開啟連結",
|
||||
"notifications_actions_not_supported": "網頁程式無法支援該動作",
|
||||
"notifications_actions_http_request_title": "傳送 HTTP {{method}} 到 {{url}}",
|
||||
"notifications_none_for_topic_title": "尚未收到任何此主題的通知。",
|
||||
"notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
|
||||
"notifications_none_for_any_title": "尚未收到任何通知。",
|
||||
"action_bar_settings": "設定",
|
||||
"action_bar_send_test_notification": "發送測試通知",
|
||||
"action_bar_clear_notifications": "清除所有通知",
|
||||
"action_bar_show_menu": "顯示選單",
|
||||
"nav_button_documentation": "文件",
|
||||
"nav_button_publish_message": "發佈通知",
|
||||
"nav_button_muted": "通知已靜音",
|
||||
"notifications_copied_to_clipboard": "已複製到剪貼簿",
|
||||
"message_bar_publish": "發佈訊息",
|
||||
"message_bar_show_dialog": "顯示發佈對話框",
|
||||
"message_bar_error_publishing": "發佈通知時發生錯誤",
|
||||
"nav_topics_title": "訂閱主題",
|
||||
"nav_button_all_notifications": "所有通知",
|
||||
"nav_button_settings": "設定",
|
||||
"nav_button_subscribe": "訂閱主題",
|
||||
"nav_button_connecting": "連線中",
|
||||
"alert_grant_title": "通知已關閉",
|
||||
"alert_not_supported_title": "不支援通知",
|
||||
"alert_not_supported_description": "瀏覽器不支援通知。",
|
||||
"notifications_tags": "標籤",
|
||||
"notifications_priority_x": "優先度 {{priority}}",
|
||||
"notifications_new_indicator": "新通知",
|
||||
"notifications_attachment_file_audio": "聲音檔案",
|
||||
"notifications_delete": "刪除",
|
||||
"notifications_attachment_link_expires": "連結在 {{date}} 過期",
|
||||
"notifications_attachment_file_image": "圖片檔案",
|
||||
"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>裡面告訴我們。"
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
basicAuth,
|
||||
encodeBase64,
|
||||
fetchLinesIterator,
|
||||
maybeWithBasicAuth,
|
||||
topicShortUrl,
|
||||
topicUrl,
|
||||
topicUrlAuth,
|
||||
topicUrlJsonPoll,
|
||||
topicUrlJsonPollWithSince, userStatsUrl
|
||||
topicUrlJsonPollWithSince,
|
||||
userStatsUrl
|
||||
} from "./utils";
|
||||
import userManager from "./UserManager";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicShortUrl} from "./utils";
|
||||
import {formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl} from "./utils";
|
||||
import prefs from "./Prefs";
|
||||
import subscriptionManager from "./SubscriptionManager";
|
||||
import logo from "../img/ntfy.png";
|
||||
@@ -18,8 +18,9 @@ class Notifier {
|
||||
return;
|
||||
}
|
||||
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||
const displayName = topicDisplayName(subscription);
|
||||
const message = formatMessage(notification);
|
||||
const title = formatTitleWithDefault(notification, shortUrl);
|
||||
const title = formatTitleWithDefault(notification, displayName);
|
||||
|
||||
// Show notification
|
||||
console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
|
||||
|
||||
@@ -133,6 +133,12 @@ class SubscriptionManager {
|
||||
});
|
||||
}
|
||||
|
||||
async setDisplayName(subscriptionId, displayName) {
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
displayName: displayName
|
||||
});
|
||||
}
|
||||
|
||||
async pruneNotifications(thresholdTimestamp) {
|
||||
await db.notifications
|
||||
.where("time").below(thresholdTimestamp)
|
||||
|
||||
@@ -38,6 +38,15 @@ export const disallowedTopic = (topic) => {
|
||||
return config.disallowedTopics.includes(topic);
|
||||
}
|
||||
|
||||
export const topicDisplayName = (subscription) => {
|
||||
if (subscription.displayName) {
|
||||
return subscription.displayName;
|
||||
} else if (subscription.baseUrl === window.location.origin) {
|
||||
return subscription.topic;
|
||||
}
|
||||
return topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||
};
|
||||
|
||||
// Format emojis (see emoji.js)
|
||||
const emojis = {};
|
||||
rawEmojis.forEach(emoji => {
|
||||
@@ -85,7 +94,6 @@ export const unmatchedTags = (tags) => {
|
||||
else return tags.filter(tag => !(tag in emojis));
|
||||
}
|
||||
|
||||
|
||||
export const maybeWithBasicAuth = (headers, user) => {
|
||||
if (user) {
|
||||
headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
|
||||
@@ -232,3 +240,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import {formatShortDateTime, shuffle, topicShortUrl} from "../app/utils";
|
||||
import {formatShortDateTime, shuffle, topicDisplayName, topicShortUrl} from "../app/utils";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||
import Grow from '@mui/material/Grow';
|
||||
@@ -24,13 +24,14 @@ import subscriptionManager from "../app/SubscriptionManager";
|
||||
import logo from "../img/ntfy.svg";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Portal, Snackbar} from "@mui/material";
|
||||
import SubscriptionSettingsDialog from "./SubscriptionSettingsDialog";
|
||||
|
||||
const ActionBar = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
let title = "ntfy";
|
||||
if (props.selected) {
|
||||
title = topicShortUrl(props.selected.baseUrl, props.selected.topic);
|
||||
title = topicDisplayName(props.selected);
|
||||
} else if (location.pathname === "/settings") {
|
||||
title = t("action_bar_settings");
|
||||
}
|
||||
@@ -79,6 +80,7 @@ const SettingsIcons = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [snackOpen, setSnackOpen] = useState(false);
|
||||
const [subscriptionSettingsOpen, setSubscriptionSettingsOpen] = useState(false);
|
||||
const anchorRef = useRef(null);
|
||||
const subscription = props.subscription;
|
||||
|
||||
@@ -116,6 +118,10 @@ const SettingsIcons = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscriptionSettings = async () => {
|
||||
setSubscriptionSettingsOpen(true);
|
||||
}
|
||||
|
||||
const handleSendTestMessage = async () => {
|
||||
const baseUrl = props.subscription.baseUrl;
|
||||
const topic = props.subscription.topic;
|
||||
@@ -201,6 +207,7 @@ const SettingsIcons = (props) => {
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}>
|
||||
<MenuItem onClick={handleSubscriptionSettings}>{t("action_bar_subscription_settings")}</MenuItem>
|
||||
<MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem>
|
||||
<MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem>
|
||||
<MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem>
|
||||
@@ -218,6 +225,14 @@ const SettingsIcons = (props) => {
|
||||
message={t("message_bar_error_publishing")}
|
||||
/>
|
||||
</Portal>
|
||||
<Portal>
|
||||
<SubscriptionSettingsDialog
|
||||
key={`subscriptionSettingsDialog${subscription.id}`}
|
||||
open={subscriptionSettingsOpen}
|
||||
subscription={subscription}
|
||||
onClose={() => setSubscriptionSettingsOpen(false)}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import SubscribeDialog from "./SubscribeDialog";
|
||||
import {Alert, AlertTitle, Badge, CircularProgress, Link, ListSubheader} from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {openUrl, topicShortUrl, topicUrl} from "../app/utils";
|
||||
import {openUrl, topicDisplayName, topicUrl} from "../app/utils";
|
||||
import routes from "./routes";
|
||||
import {ConnectionState} from "../app/Connection";
|
||||
import {useLocation, useNavigate} from "react-router-dom";
|
||||
@@ -173,12 +173,10 @@ const SubscriptionItem = (props) => {
|
||||
const icon = (subscription.state === ConnectionState.Connecting)
|
||||
? <CircularProgress size="24px"/>
|
||||
: <Badge badgeContent={iconBadge} invisible={subscription.new === 0} color="primary"><ChatBubbleOutlineIcon/></Badge>;
|
||||
const label = (subscription.baseUrl === window.location.origin)
|
||||
? subscription.topic
|
||||
: topicShortUrl(subscription.baseUrl, subscription.topic);
|
||||
const displayName = topicDisplayName(subscription);
|
||||
const ariaLabel = (subscription.state === ConnectionState.Connecting)
|
||||
? `${label} (${t("nav_button_connecting")})`
|
||||
: label;
|
||||
? `${displayName} (${t("nav_button_connecting")})`
|
||||
: displayName;
|
||||
const handleClick = async () => {
|
||||
navigate(routes.forSubscription(subscription));
|
||||
await subscriptionManager.markNotificationsRead(subscription.id);
|
||||
@@ -186,7 +184,7 @@ const SubscriptionItem = (props) => {
|
||||
return (
|
||||
<ListItemButton onClick={handleClick} selected={props.selected} aria-label={ariaLabel} aria-live="polite">
|
||||
<ListItemIcon>{icon}</ListItemIcon>
|
||||
<ListItemText primary={label}/>
|
||||
<ListItemText primary={displayName}/>
|
||||
{subscription.mutedUntil > 0 &&
|
||||
<ListItemIcon edge="end" aria-label={t("nav_button_muted")}><NotificationsOffOutlined /></ListItemIcon>}
|
||||
</ListItemButton>
|
||||
|
||||
@@ -399,7 +399,9 @@ const performHttpAction = async (notification, action) => {
|
||||
const response = await fetch(action.url, {
|
||||
method: action.method ?? "POST",
|
||||
headers: action.headers ?? {},
|
||||
body: action.body ?? ""
|
||||
// This must not null-coalesce to a non nullish value. Otherwise, the fetch API
|
||||
// will reject it for "having a body"
|
||||
body: action.body
|
||||
});
|
||||
console.log(`[Notifications] HTTP user action response`, response);
|
||||
const success = response.status >= 200 && response.status <= 299;
|
||||
|
||||
@@ -436,7 +436,7 @@ const Appearance = () => {
|
||||
const Language = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const labelId = "prefLanguage";
|
||||
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
||||
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇵🇱", "🇺🇦", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
||||
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
|
||||
const lang = i18n.language ?? "en";
|
||||
|
||||
@@ -458,10 +458,13 @@ const Language = () => {
|
||||
<MenuItem value="fr">Français</MenuItem>
|
||||
<MenuItem value="it">Italiano</MenuItem>
|
||||
<MenuItem value="hu">Magyar</MenuItem>
|
||||
<MenuItem value="ko">한국어</MenuItem>
|
||||
<MenuItem value="ja">日本語</MenuItem>
|
||||
<MenuItem value="nl">Nederlands</MenuItem>
|
||||
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
||||
<MenuItem value="uk">Українська</MenuItem>
|
||||
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
|
||||
<MenuItem value="pl">Polski</MenuItem>
|
||||
<MenuItem value="ru">Русский</MenuItem>
|
||||
<MenuItem value="tr">Türkçe</MenuItem>
|
||||
</Select>
|
||||
|
||||
@@ -90,6 +90,14 @@ const PublishDialog = (props) => {
|
||||
setMessage(props.message);
|
||||
}, [props.message]);
|
||||
|
||||
const updateBaseUrl = (newVal) => {
|
||||
if (validUrl(newVal)) {
|
||||
setBaseUrl(newVal.replace(/\/$/, '')); // strip traililng slash after https?://
|
||||
} else {
|
||||
setBaseUrl(newVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const url = new URL(topicUrl(baseUrl, topic));
|
||||
if (title.trim()) {
|
||||
@@ -242,7 +250,7 @@ const PublishDialog = (props) => {
|
||||
label={t("publish_dialog_base_url_label")}
|
||||
placeholder={t("publish_dialog_base_url_placeholder")}
|
||||
value={baseUrl}
|
||||
onChange={ev => setBaseUrl(ev.target.value)}
|
||||
onChange={ev => updateBaseUrl(ev.target.value)}
|
||||
disabled={disabled}
|
||||
type="url"
|
||||
variant="standard"
|
||||
|
||||
@@ -9,7 +9,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
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 subscriptionManager from "../app/SubscriptionManager";
|
||||
import poller from "../app/Poller";
|
||||
@@ -90,6 +90,13 @@ const SubscribePage = (props) => {
|
||||
return validTopic(topic) && !isExistingTopicUrl;
|
||||
}
|
||||
})();
|
||||
const updateBaseUrl = (ev, newVal) => {
|
||||
if (validUrl(newVal)) {
|
||||
props.setBaseUrl(newVal.replace(/\/$/, '')); // strip trailing slash after https?://
|
||||
} else {
|
||||
props.setBaseUrl(newVal);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
|
||||
@@ -97,21 +104,26 @@ const SubscribePage = (props) => {
|
||||
<DialogContentText>
|
||||
{t("subscribe_dialog_subscribe_description")}
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="topic"
|
||||
placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
|
||||
value={props.topic}
|
||||
onChange={ev => props.setTopic(ev.target.value)}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
maxLength: 64,
|
||||
"aria-label": t("subscribe_dialog_subscribe_topic_placeholder")
|
||||
}}
|
||||
/>
|
||||
<div style={{display: 'flex'}} role="row">
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="topic"
|
||||
placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
|
||||
value={props.topic}
|
||||
onChange={ev => props.setTopic(ev.target.value)}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
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
|
||||
sx={{pt: 1}}
|
||||
control={
|
||||
@@ -128,7 +140,7 @@ const SubscribePage = (props) => {
|
||||
options={existingBaseUrls}
|
||||
sx={{ maxWidth: 400 }}
|
||||
inputValue={props.baseUrl}
|
||||
onInputChange={(ev, newVal) => props.setBaseUrl(newVal)}
|
||||
onInputChange={updateBaseUrl}
|
||||
renderInput={ (params) =>
|
||||
<TextField
|
||||
{...params}
|
||||
|
||||
59
web/src/components/SubscriptionSettingsDialog.js
Normal file
59
web/src/components/SubscriptionSettingsDialog.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {Autocomplete, Checkbox, FormControlLabel, useMediaQuery} from "@mui/material";
|
||||
import theme from "./theme";
|
||||
import api from "../app/Api";
|
||||
import {topicUrl, validTopic, validUrl} from "../app/utils";
|
||||
import userManager from "../app/UserManager";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import poller from "../app/Poller";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const SubscriptionSettingsDialog = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const subscription = props.subscription;
|
||||
const [displayName, setDisplayName] = useState(subscription.displayName ?? "");
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const handleSave = async () => {
|
||||
await subscriptionManager.setDisplayName(subscription.id, displayName);
|
||||
props.onClose();
|
||||
}
|
||||
return (
|
||||
<Dialog open={props.open} onClose={props.onClose} fullScreen={fullScreen}>
|
||||
<DialogTitle>{t("subscription_settings_dialog_title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t("subscription_settings_dialog_description")}
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="topic"
|
||||
placeholder={t("subscription_settings_dialog_display_name_placeholder")}
|
||||
value={displayName}
|
||||
onChange={ev => setDisplayName(ev.target.value)}
|
||||
type="text"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
inputProps={{
|
||||
maxLength: 64,
|
||||
"aria-label": t("subscription_settings_dialog_display_name_placeholder")
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button onClick={props.onClose}>{t("subscription_settings_button_cancel")}</Button>
|
||||
<Button onClick={handleSave}>{t("subscription_settings_button_save")}</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionSettingsDialog;
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user