mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-20 00:57:24 +01:00
Compare commits
319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396e61cdb3 | ||
|
|
dfaab8c386 | ||
|
|
f2f5a06be1 | ||
|
|
8d7ff4d7db | ||
|
|
9f052bdf8b | ||
|
|
5472c8513f | ||
|
|
c028ec9083 | ||
|
|
31a87935a5 | ||
|
|
9e20ee35e1 | ||
|
|
0d4ef18358 | ||
|
|
8bde80a3d2 | ||
|
|
020f561ad4 | ||
|
|
432cc2003e | ||
|
|
aea8a6d04b | ||
|
|
e449f0bda4 | ||
|
|
ff3cb6c5cc | ||
|
|
2b4f7ab56f | ||
|
|
f5a8216be6 | ||
|
|
3779b4a923 | ||
|
|
9738e4a225 | ||
|
|
0905016b1f | ||
|
|
5f8ecfaf81 | ||
|
|
8da46afab4 | ||
|
|
facf4684ae | ||
|
|
2624897efe | ||
|
|
df6f53a161 | ||
|
|
b941551fff | ||
|
|
471775ae49 | ||
|
|
3d84bdf77b | ||
|
|
8668143127 | ||
|
|
295bad59bb | ||
|
|
804ee3b298 | ||
|
|
75c07221ef | ||
|
|
b82794df05 | ||
|
|
167656b38e | ||
|
|
5d81f875cb | ||
|
|
6ae200e338 | ||
|
|
ab6b902fb5 | ||
|
|
9f423b01ef | ||
|
|
c863c86f4c | ||
|
|
2bd27a5d0b | ||
|
|
cff8f88920 | ||
|
|
87f5479662 | ||
|
|
a589705e6d | ||
|
|
ee062c13d4 | ||
|
|
30645bc4e0 | ||
|
|
0dd07d10a0 | ||
|
|
1fd166d5c7 | ||
|
|
96599df89f | ||
|
|
2ec13c64f3 | ||
|
|
c916eeb9d7 | ||
|
|
8ee85a4007 | ||
|
|
1aa716de55 | ||
|
|
f631bdc782 | ||
|
|
81cb055375 | ||
|
|
b4a42602e2 | ||
|
|
57171f57e4 | ||
|
|
82df434d19 | ||
|
|
264deab637 | ||
|
|
69345ed26c | ||
|
|
36c0be1097 | ||
|
|
e12bc6aa19 | ||
|
|
64d4d64aa7 | ||
|
|
757f1484e9 | ||
|
|
7979608cc5 | ||
|
|
1918f7f0aa | ||
|
|
ea0c9c65d9 | ||
|
|
8aec85c579 | ||
|
|
4fa03f4938 | ||
|
|
e0a957c4e9 | ||
|
|
5db72e5fee | ||
|
|
8ce2fff8ab | ||
|
|
5a24e30820 | ||
|
|
b78efdd155 | ||
|
|
dab18e5b40 | ||
|
|
66c8f8d8df | ||
|
|
dd282963c3 | ||
|
|
d023a81a32 | ||
|
|
73e8f955ca | ||
|
|
5e7657fc40 | ||
|
|
76b4d4c10c | ||
|
|
b3c975314d | ||
|
|
4e7e6e57fe | ||
|
|
0b78d3173d | ||
|
|
92d7e5c58a | ||
|
|
632d013fb8 | ||
|
|
207894dac6 | ||
|
|
6f170b1ad7 | ||
|
|
6dbe25fcc5 | ||
|
|
74828adcb8 | ||
|
|
3120cd54fe | ||
|
|
b1cafc06eb | ||
|
|
fd66fb33a8 | ||
|
|
5af9d0164b | ||
|
|
049a01d58f | ||
|
|
629af0efc3 | ||
|
|
a1262c2406 | ||
|
|
97dd879597 | ||
|
|
f1321d6140 | ||
|
|
0646f48ca6 | ||
|
|
a50d65393e | ||
|
|
67221b015d | ||
|
|
40aadbad85 | ||
|
|
77ebf306a3 | ||
|
|
94d3924432 | ||
|
|
1235ea5bb5 | ||
|
|
321ed12663 | ||
|
|
265af01f9c | ||
|
|
a9961df4e2 | ||
|
|
8d3f35f4f7 | ||
|
|
2b8ae406a3 | ||
|
|
d78f1a3ff9 | ||
|
|
c500c9c199 | ||
|
|
b2363d2783 | ||
|
|
8aba600fa5 | ||
|
|
18596ecc34 | ||
|
|
420d289d35 | ||
|
|
eebd0f113b | ||
|
|
c4286984ab | ||
|
|
e0d6a0b974 | ||
|
|
71e46860ac | ||
|
|
ce942ffe16 | ||
|
|
e083ef0d6d | ||
|
|
b91fb3f586 | ||
|
|
79356baee1 | ||
|
|
cb6c0b6e45 | ||
|
|
543bc24bfd | ||
|
|
789ff72081 | ||
|
|
5dc4754181 | ||
|
|
eaa64b636a | ||
|
|
1c9cd40d34 | ||
|
|
9c54181ff8 | ||
|
|
d4211441b3 | ||
|
|
3307debacc | ||
|
|
95fd6ecab1 | ||
|
|
84dca41008 | ||
|
|
b3d90f04ac | ||
|
|
c2550dbca9 | ||
|
|
fe11ed3ac7 | ||
|
|
24b5eb3405 | ||
|
|
bc16c49187 | ||
|
|
3438e0bfb0 | ||
|
|
7e9abd2350 | ||
|
|
8f6880d809 | ||
|
|
e0024e59f3 | ||
|
|
b9b604c007 | ||
|
|
be6c30fb0d | ||
|
|
7001543d28 | ||
|
|
bc38c08a5e | ||
|
|
7f49ebb4ec | ||
|
|
3746d2935b | ||
|
|
7b6577d543 | ||
|
|
f6643ebc12 | ||
|
|
fd9ab2704c | ||
|
|
f241003ac6 | ||
|
|
38f7843861 | ||
|
|
25e95ae1a6 | ||
|
|
4c1c5e56ab | ||
|
|
ed29b675ee | ||
|
|
3d501ceaf9 | ||
|
|
c5b2c8c680 | ||
|
|
f29fe22d3d | ||
|
|
2540a0396d | ||
|
|
9fec3f35ff | ||
|
|
679b075ecc | ||
|
|
b1819d4766 | ||
|
|
96b7053884 | ||
|
|
fcbf71dad7 | ||
|
|
aee791a17d | ||
|
|
5b2fe66903 | ||
|
|
f4daa4508f | ||
|
|
755155479a | ||
|
|
978118a400 | ||
|
|
4a91da60dd | ||
|
|
db9ca80b69 | ||
|
|
e147a41f92 | ||
|
|
497f871447 | ||
|
|
ad860afb8b | ||
|
|
b4933a5645 | ||
|
|
46f437126c | ||
|
|
90b85f2956 | ||
|
|
ebfbf7cc8e | ||
|
|
499ac76c43 | ||
|
|
fd7f83378d | ||
|
|
e7b575badc | ||
|
|
a0f2d81337 | ||
|
|
fb6980a81e | ||
|
|
df45459618 | ||
|
|
61b2d92595 | ||
|
|
adda27ec57 | ||
|
|
b92b5b37fb | ||
|
|
18d36e1b30 | ||
|
|
f4cb447f0a | ||
|
|
069617eba0 | ||
|
|
aff193a003 | ||
|
|
eb6a86a009 | ||
|
|
97025fe8ef | ||
|
|
08bb0103e8 | ||
|
|
e02789c70c | ||
|
|
cf7a451198 | ||
|
|
f088498f26 | ||
|
|
bcc20e0aec | ||
|
|
e236214fd5 | ||
|
|
b103caf9d4 | ||
|
|
a43a4aea5e | ||
|
|
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 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +1,2 @@
|
||||
github: [binwiederhier]
|
||||
liberapay: ntfy
|
||||
|
||||
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.18.x'
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
|
||||
36
.github/workflows/docs.yaml
vendored
Normal file
36
.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
publish-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout ntfy code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Checkout docs pages code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: binwiederhier/ntfy-docs.github.io
|
||||
path: build/ntfy-docs.github.io
|
||||
token: ${{secrets.NTFY_DOCS_PUSH_TOKEN}}
|
||||
# Expires after 1 year, re-generate via
|
||||
# User -> Settings -> Developer options -> Personal Access Tokens -> Fine Grained Token
|
||||
-
|
||||
name: Build docs
|
||||
run: make docs
|
||||
-
|
||||
name: Copy generated docs
|
||||
run: rsync -av --exclude CNAME --delete server/docs/ build/ntfy-docs.github.io/docs/
|
||||
-
|
||||
name: Publish docs
|
||||
run: |
|
||||
cd build/ntfy-docs.github.io
|
||||
git config user.name "GitHub Actions Bot"
|
||||
git config user.email "<>"
|
||||
git add docs/
|
||||
git commit -m "Updated docs"
|
||||
git push origin main
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.18.x'
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.18.x'
|
||||
go-version: '1.19.x'
|
||||
-
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
|
||||
28
.gitpod.yml
Normal file
28
.gitpod.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
tasks:
|
||||
- name: docs
|
||||
before: make docs-deps
|
||||
command: mkdocs serve
|
||||
- name: binary
|
||||
before: |
|
||||
npm install --global nodemon
|
||||
make cli-deps-static-sites
|
||||
command: |
|
||||
nodemon --watch './**/*.go' --ext go --signal SIGTERM --exec "CGO_ENABLED=1 go run main.go serve --listen-http :2586 --debug --base-url $(gp url 2586)"
|
||||
openMode: split-right
|
||||
- name: web
|
||||
before: make web-deps
|
||||
command: cd web && npm start
|
||||
openMode: split-right
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- golang.go
|
||||
- ms-azuretools.vscode-docker
|
||||
|
||||
ports:
|
||||
- name: docs
|
||||
port: 8000
|
||||
- name: binary
|
||||
port: 2586
|
||||
- name: web
|
||||
port: 3000
|
||||
@@ -13,9 +13,6 @@ builds:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [amd64]
|
||||
hooks:
|
||||
post:
|
||||
- upx "{{ .Path }}" # apt install upx
|
||||
-
|
||||
id: ntfy_linux_armv6
|
||||
binary: ntfy
|
||||
@@ -28,7 +25,6 @@ builds:
|
||||
goos: [linux]
|
||||
goarch: [arm]
|
||||
goarm: [6]
|
||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||
-
|
||||
id: ntfy_linux_armv7
|
||||
binary: ntfy
|
||||
@@ -41,7 +37,6 @@ builds:
|
||||
goos: [linux]
|
||||
goarch: [arm]
|
||||
goarm: [7]
|
||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||
-
|
||||
id: ntfy_linux_arm64
|
||||
binary: ntfy
|
||||
@@ -53,7 +48,6 @@ builds:
|
||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [linux]
|
||||
goarch: [arm64]
|
||||
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||
-
|
||||
id: ntfy_windows_amd64
|
||||
binary: ntfy
|
||||
@@ -64,7 +58,6 @@ builds:
|
||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||
goos: [windows]
|
||||
goarch: [amd64]
|
||||
# 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
|
||||
|
||||
@@ -3,5 +3,7 @@ MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
|
||||
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
HEALTHCHECK --interval=60s --timeout=10s CMD wget -q --tries=1 http://localhost/v1/health -O - | grep -Eo '"healthy"\s*:\s*true' || exit 1
|
||||
|
||||
EXPOSE 80/tcp
|
||||
ENTRYPOINT ["ntfy"]
|
||||
|
||||
2
Makefile
2
Makefile
@@ -88,7 +88,6 @@ build-deps-ubuntu:
|
||||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabi \
|
||||
upx \
|
||||
jq
|
||||
which pip3 || sudo apt install -y python3-pip
|
||||
|
||||
@@ -201,7 +200,6 @@ cli-deps-static-sites:
|
||||
touch server/docs/index.html server/site/app.html
|
||||
|
||||
cli-deps-all:
|
||||
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
|
||||
cli-deps-gcc-armv6-armv7:
|
||||
|
||||
101
README.md
101
README.md
@@ -1,13 +1,5 @@
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 👶 Baby break - My baby girl was born!
|
||||
Hey folks, my daughter was born on 8/30/22, so I'll be taking some time off from working on ntfy. I'll likely return
|
||||
to working on features and bugs in a few weeks. I hope you understand. I posted some pictures in [#387](https://github.com/binwiederhier/ntfy/issues/387) 🥰
|
||||
|
||||
---
|
||||
|
||||
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
||||
[](https://github.com/binwiederhier/ntfy/releases/latest)
|
||||
[](https://pkg.go.dev/heckel.io/ntfy)
|
||||
@@ -17,7 +9,9 @@ to working on features and bugs in a few weeks. I hope you understand. I posted
|
||||
[](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**.
|
||||
@@ -41,10 +35,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)
|
||||
@@ -52,28 +51,84 @@ 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>
|
||||
|
||||
## Donations
|
||||
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:
|
||||
## Sponsors
|
||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
||||
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
||||
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
||||
|
||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.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/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
|
||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
|
||||
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
|
||||
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
|
||||
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
|
||||
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
|
||||
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
|
||||
<a href="https://github.com/Lexevolution"><img src="https://github.com/Lexevolution.png" width="40px" /></a>
|
||||
<a href="https://github.com/johnnyip"><img src="https://github.com/johnnyip.png" width="40px" /></a>
|
||||
<a href="https://github.com/JonDerThan"><img src="https://github.com/JonDerThan.png" width="40px" /></a>
|
||||
<a href="https://github.com/12nick12"><img src="https://github.com/12nick12.png" width="40px" /></a>
|
||||
<a href="https://github.com/eanplatter"><img src="https://github.com/eanplatter.png" width="40px" /></a>
|
||||
<a href="https://github.com/fnoelscher"><img src="https://github.com/fnoelscher.png" width="40px" /></a>
|
||||
<a href="https://github.com/bnorick"><img src="https://github.com/bnorick.png" width="40px" /></a>
|
||||
<a href="https://github.com/snh"><img src="https://github.com/snh.png" width="40px" /></a>
|
||||
<a href="https://github.com/hen-x"><img src="https://github.com/hen-x.png" width="40px" /></a>
|
||||
<a href="https://github.com/JamieGoodson"><img src="https://github.com/JamieGoodson.png" width="40px" /></a>
|
||||
<a href="https://github.com/cremesk"><img src="https://github.com/cremesk.png" width="40px" /></a>
|
||||
<a href="https://github.com/dangowans"><img src="https://github.com/dangowans.png" width="40px" /></a>
|
||||
<a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
|
||||
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
|
||||
<a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
|
||||
<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
|
||||
<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
|
||||
<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
|
||||
<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
|
||||
<a href="https://github.com/tonyakwei"><img src="https://github.com/tonyakwei.png" width="40px" /></a>
|
||||
<a href="https://github.com/crosbyh"><img src="https://github.com/crosbyh.png" width="40px" /></a>
|
||||
<a href="https://github.com/mdlnr"><img src="https://github.com/mdlnr.png" width="40px" /></a>
|
||||
<a href="https://github.com/p-samuel"><img src="https://github.com/p-samuel.png" width="40px" /></a>
|
||||
<a href="https://github.com/zugaldia"><img src="https://github.com/zugaldia.png" width="40px" /></a>
|
||||
<a href="https://github.com/NathanSweet"><img src="https://github.com/NathanSweet.png" width="40px" /></a>
|
||||
<a href="https://github.com/msdeibel"><img src="https://github.com/msdeibel.png" width="40px" /></a>
|
||||
<a href="https://github.com/ksurl"><img src="https://github.com/ksurl.png" width="40px" /></a>
|
||||
<a href="https://github.com/CodingTimeDEV"><img src="https://github.com/CodingTimeDEV.png" width="40px" /></a>
|
||||
<a href="https://github.com/Terrormixer3000"><img src="https://github.com/Terrormixer3000.png" width="40px" /></a>
|
||||
<a href="https://github.com/voroskoi"><img src="https://github.com/voroskoi.png" width="40px" /></a>
|
||||
<a href="https://github.com/Nickwasused"><img src="https://github.com/Nickwasused.png" width="40px" /></a>
|
||||
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
|
||||
<a href="https://github.com/vinhdizzo"><img src="https://github.com/vinhdizzo.png" width="40px" /></a>
|
||||
<a href="https://github.com/Ge0rg3"><img src="https://github.com/Ge0rg3.png" width="40px" /></a>
|
||||
<a href="https://github.com/biopsin"><img src="https://github.com/biopsin.png" width="40px" /></a>
|
||||
<a href="https://github.com/thebino"><img src="https://github.com/thebino.png" width="40px" /></a>
|
||||
<a href="https://github.com/sky4055"><img src="https://github.com/sky4055.png" width="40px" /></a>
|
||||
<a href="https://github.com/julianlam"><img src="https://github.com/julianlam.png" width="40px" /></a>
|
||||
<a href="https://github.com/andreapx"><img src="https://github.com/andreapx.png" width="40px" /></a>
|
||||
<a href="https://github.com/billycao"><img src="https://github.com/billycao.png" width="40px" /></a>
|
||||
<a href="https://github.com/zoic21"><img src="https://github.com/zoic21.png" width="40px" /></a>
|
||||
<a href="https://github.com/IanKulin"><img src="https://github.com/IanKulin.png" width="40px" /></a>
|
||||
<a href="https://github.com/Joachim256"><img src="https://github.com/Joachim256.png" width="40px" /></a>
|
||||
<a href="https://github.com/overtone1000"><img src="https://github.com/overtone1000.png" width="40px" /></a>
|
||||
|
||||
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
|
||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||
|
||||
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||
|
||||
## Code of Conduct
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
|
||||
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
|
||||
|
||||
_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
||||
|
||||
## License
|
||||
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
#
|
||||
# default-host: https://ntfy.sh
|
||||
|
||||
# Defaults below will be used when a topic does not have its own settings
|
||||
# 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,
|
||||
|
||||
@@ -12,14 +12,14 @@ const (
|
||||
|
||||
// Config is the config struct for a Client
|
||||
type Config struct {
|
||||
DefaultHost string `yaml:"default-host"`
|
||||
DefaultUser string `yaml:"default-user"`
|
||||
DefaultPassword string `yaml:"default-password"`
|
||||
DefaultCommand string `yaml:"default-command"`
|
||||
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"`
|
||||
@@ -30,7 +30,7 @@ func NewConfig() *Config {
|
||||
return &Config{
|
||||
DefaultHost: DefaultBaseURL,
|
||||
DefaultUser: "",
|
||||
DefaultPassword: "",
|
||||
DefaultPassword: nil,
|
||||
DefaultCommand: "",
|
||||
Subscribe: nil,
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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: phil
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
default-command: 'echo "Got the message: $message"'
|
||||
subscribe:
|
||||
@@ -31,14 +31,14 @@ subscribe:
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, "phil", conf.DefaultUser)
|
||||
require.Equal(t, "mypass", conf.DefaultPassword)
|
||||
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)
|
||||
@@ -46,3 +46,73 @@ subscribe:
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -49,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,
|
||||
@@ -72,7 +72,7 @@ Examples:
|
||||
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
|
||||
|
||||
@@ -160,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)
|
||||
@@ -239,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 {
|
||||
|
||||
@@ -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"}))
|
||||
|
||||
43
cmd/serve.go
43
cmd/serve.go
@@ -5,16 +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"
|
||||
@@ -42,6 +44,8 @@ var flagsServe = append(
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "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"}),
|
||||
@@ -108,6 +112,8 @@ func execServe(c *cli.Context) error {
|
||||
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")
|
||||
@@ -208,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
|
||||
@@ -233,6 +237,8 @@ func execServe(c *cli.Context) error {
|
||||
conf.CacheFile = cacheFile
|
||||
conf.CacheDuration = cacheDuration
|
||||
conf.CacheStartupQueries = cacheStartupQueries
|
||||
conf.CacheBatchSize = cacheBatchSize
|
||||
conf.CacheBatchTimeout = cacheBatchTimeout
|
||||
conf.AuthFile = authFile
|
||||
conf.AuthDefaultRead = authDefaultRead
|
||||
conf.AuthDefaultWrite = authDefaultWrite
|
||||
@@ -303,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,7 +29,7 @@ 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.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,19 +175,20 @@ 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))
|
||||
}
|
||||
var user, password string
|
||||
var user string
|
||||
var password *string
|
||||
if s.User != "" {
|
||||
user = s.User
|
||||
} else if conf.DefaultUser != "" {
|
||||
user = conf.DefaultUser
|
||||
}
|
||||
if s.Password != "" {
|
||||
if s.Password != nil {
|
||||
password = s.Password
|
||||
} else if conf.DefaultPassword != "" {
|
||||
} else if conf.DefaultPassword != nil {
|
||||
password = conf.DefaultPassword
|
||||
}
|
||||
if user != "" && password != "" {
|
||||
topicOptions = append(topicOptions, client.WithBasicAuth(user, password))
|
||||
if user != "" && password != nil {
|
||||
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
|
||||
}
|
||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||
if s.Command != "" {
|
||||
|
||||
139
docs/config.md
139
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,16 @@ 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 pretty much how ntfy.sh is configured. To see the exact configuration,
|
||||
# see https://github.com/binwiederhier/ntfy-ansible/
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ntfy.sh;
|
||||
@@ -482,14 +509,17 @@ or the root domain:
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen 443 ssl http2;
|
||||
server_name ntfy.sh;
|
||||
|
||||
ssl_session_cache builtin:1000 shared:SSL:10m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
|
||||
|
||||
@@ -515,8 +545,73 @@ 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 http2;
|
||||
server_name ntfy.sh;
|
||||
|
||||
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
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 +750,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 +832,27 @@ out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/
|
||||
|
||||
Depending on *how you run it*, here are a few limits that are relevant:
|
||||
|
||||
### 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
|
||||
@@ -807,7 +914,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
```
|
||||
# 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
|
||||
@@ -822,7 +929,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
1 $binary_remote_addr;
|
||||
0 "";
|
||||
}
|
||||
limit_req_zone $limitkey zone=one:10m rate=1r/s;
|
||||
limit_req_zone $limitkey zone=one:10m rate=45r/m;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -851,7 +958,7 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
|
||||
logpath = /var/log/nginx/error.log
|
||||
findtime = 600
|
||||
bantime = 7200
|
||||
bantime = 14400
|
||||
maxretry = 10
|
||||
```
|
||||
|
||||
@@ -898,6 +1005,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
||||
| `cache-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. |
|
||||
@@ -911,9 +1020,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`. |
|
||||
@@ -962,6 +1071,8 @@ OPTIONS:
|
||||
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
||||
--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)
|
||||
@@ -85,7 +92,6 @@ sudo apt install \
|
||||
gcc-arm-linux-gnueabi \
|
||||
gcc-aarch64-linux-gnu \
|
||||
python3-pip \
|
||||
upx \
|
||||
git
|
||||
```
|
||||
|
||||
@@ -321,7 +327,76 @@ To build your own version with Firebase, you must:
|
||||
```
|
||||
|
||||
## iOS app
|
||||
The ntfy iOS app source code is available [on GitHub](https://github.com/binwiederhier/ntfy-ios).
|
||||
Building the iOS app is very involved. Please report any inconsistencies or issues with it. The requirements are
|
||||
strictly based off of my development on this app. There may be other versions of macOS / XCode that work.
|
||||
|
||||
### Requirements
|
||||
1. macOS Monterey or later
|
||||
1. XCode 13.2+
|
||||
1. A physical iOS device (for push notifications, Firebase does not work in the XCode simulator)
|
||||
1. Firebase account
|
||||
1. Apple Developer license? (I forget if it's possible to do testing without purchasing the license)
|
||||
|
||||
### Apple setup
|
||||
|
||||
!!! info
|
||||
I haven't had time to move the build instructions here. Please check out the repository instead.
|
||||
Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required
|
||||
for these changes to take effect in the iOS app.
|
||||
|
||||
1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add)
|
||||
1. Select "Apple Push Notifications service (APNs)"
|
||||
1. Download the newly created key (should have a file name similar to `AuthKey_ZZZZZZ.p8`, where `ZZZZZZ` is the **Key ID**)
|
||||
1. Record your **Team ID** - it can be seen in the top-right corner of the page, or on your Account > Membership page
|
||||
1. Next, navigate to "Project Settings" in the firebase console for your project, and select the iOS app you created. Then, click "Cloud Messaging" in the left sidebar, and scroll down to the "APNs Authentication Key" section. Click "Upload Key", and upload the key you downloaded from Apple Developer.
|
||||
|
||||
!!! warning
|
||||
If you don't do the above setups for APNS, **notifications will not post instantly or sometimes at all**. This is because of the missing APNS key, which is required for firebase to send notifications to the iOS app. See below for a snip from the firebase docs.
|
||||
|
||||
If you don't have an APNs authentication key, you can still send notifications to iOS devices, but they won't be delivered
|
||||
instantly. Instead, they'll be delivered when the device wakes up to check for new notifications or when your application
|
||||
sends a firebase request to check for them. The time to check for new notifications can vary from a few seconds to hours,
|
||||
days or even weeks. Enabling APNs authentication keys ensures that notifications are delivered instantly and is strongly
|
||||
recommended.
|
||||
|
||||
### Firebase setup
|
||||
|
||||
1. If you haven't already, create a Google / Firebase account
|
||||
1. Visit the [Firebase console](https://console.firebase.google.com)
|
||||
1. Create a new Firebase project:
|
||||
1. Enter a project name
|
||||
1. Disable Google Analytics (currently iOS app does not support analytics)
|
||||
1. On the "Project settings" page, add an iOS app
|
||||
1. Apple bundle ID - "com.copephobia.ntfy-ios" (this can be changed to match XCode's ntfy.sh target > "Bundle Identifier" value)
|
||||
1. Register the app
|
||||
1. Download the config file - GoogleInfo.plist (this will need to be included in the ntfy-ios repository / XCode)
|
||||
1. Generate a new service account private key for the ntfy server
|
||||
1. Go to "Project settings" > "Service accounts"
|
||||
1. Click "Generate new private key" to generate and download a private key to use for sending messages via the ntfy server
|
||||
|
||||
### ntfy server
|
||||
Note that the ntfy server is not officially supported on macOS. It should, however, be able to run on macOS using these
|
||||
steps:
|
||||
|
||||
1. If not already made, make the `/etc/ntfy/` directory and move the service account private key to that folder
|
||||
1. Copy the `server/server.yml` file from the ntfy repository to `/etc/ntfy/`
|
||||
1. Modify the `/etc/ntfy/server.yml` file `firebase-key-file` value to the path of the private key
|
||||
1. Install go: `brew install go`
|
||||
1. In the ntfy repository, run `make cli-darwin-server`.
|
||||
|
||||
### XCode setup
|
||||
|
||||
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
|
||||
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
|
||||
1. Similarly, install the SQLite.swift package dependency in XCode
|
||||
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
||||
|
||||
### PLIST config
|
||||
To have instant notifications/better notification delivery when using firebase, you will need to add the
|
||||
`GoogleService-Info.plist` file to your project. Here's how to do that:
|
||||
|
||||
1. In XCode, find the NTFY app target. **Not** the NSE app target.
|
||||
1. Find the Asset/ folder in the project navigator
|
||||
1. Drag the `GoogleService-Info.plist` file into the Asset/ folder that you get from the firebase console. It can be
|
||||
found in the "Project settings" > "General" > "Your apps" with a button labled "GoogleService-Info.plist"
|
||||
|
||||
After that, you should be all set!
|
||||
|
||||
@@ -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,9 +408,13 @@ 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.
|
||||
JSON payload. Remember to change the `https://request.example.com` to your URL as the value of the JSON key click.
|
||||
And if you're not using the request `topic`, make sure to change it in the JSON payload to your topic.
|
||||
|
||||
``` json
|
||||
{
|
||||
@@ -504,3 +547,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`):
|
||||

|
||||
|
||||
|
||||
|
||||
36
docs/faq.md
36
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,6 +52,25 @@ decent now.
|
||||
server and listens for incoming notifications. This consumes additional battery (see above),
|
||||
but delivers notifications instantly.
|
||||
|
||||
## Can you implement feature X?
|
||||
Yes, maybe. Check out [existing GitHub issues](https://github.com/binwiederhier/ntfy/issues) to see if somebody else had
|
||||
the same idea before you, or file a new issue. I'll likely get back to you within a few days.
|
||||
|
||||
## I'm having issues with iOS, can you help? The iOS app is behind compared to the Android app, can you fix that?
|
||||
The iOS is very bare bones and quite frankly a little buggy. I wanted to get something out the door to make the iOS users
|
||||
happy, but halfway through I got frustrated with iOS development and paused development. I will eventually get back to
|
||||
it, or hopefully, somebody else will come along and help out. Please review the [known issues](known-issues.md) for details.
|
||||
|
||||
## Can I disable the web app? Can I protect it with a login screen?
|
||||
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
|
||||
|
||||
384
docs/install.md
384
docs/install.md
@@ -14,10 +14,10 @@ We support amd64, armv7 and arm64.
|
||||
|
||||
1. Install ntfy using one of the methods described below
|
||||
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (or `/etc/ntfy/client.yml`, see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||
|
||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||
To 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.28.0/ntfy_1.28.0_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_1.28.0_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_1.28.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_x86_64.tar.gz
|
||||
tar zxvf ntfy_1.31.0_linux_x86_64.tar.gz
|
||||
sudo cp -a ntfy_1.31.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_1.28.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_1.28.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_1.31.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_1.31.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_1.28.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_1.28.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_1.31.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_1.31.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_1.28.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_1.28.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_1.31.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_1.31.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.31.0_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.28.0/ntfy_1.28.0_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo 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.28.0/ntfy_1.28.0_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo 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.28.0/ntfy_1.28.0_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo 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.28.0/ntfy_1.28.0_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo 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.28.0/ntfy_1.28.0_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_amd64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv6.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_armv7.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl 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.28.0/ntfy_1.28.0_macOS_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_macOS_all.tar.gz),
|
||||
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||
|
||||
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.28.0/ntfy_1.28.0_macOS_all.tar.gz > ntfy_1.28.0_macOS_all.tar.gz
|
||||
tar zxvf ntfy_1.28.0_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_1.28.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_macOS_all.tar.gz > ntfy_1.31.0_macOS_all.tar.gz
|
||||
tar zxvf ntfy_1.31.0_macOS_all.tar.gz
|
||||
sudo cp -a ntfy_1.31.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_1.28.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_1.31.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
@@ -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.28.0/ntfy_1.28.0_windows_x86_64.zip),
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.31.0/ntfy_1.31.0_windows_x86_64.zip),
|
||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||
|
||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||
@@ -282,7 +287,7 @@ services:
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files to the same uid/gid.
|
||||
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
|
||||
|
||||
Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately.
|
||||
```
|
||||
@@ -291,3 +296,300 @@ 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
|
||||
- name: cache
|
||||
mountPath: "/var/cache/ntfy"
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -6,23 +6,34 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
|
||||
## Public ntfy servers
|
||||
|
||||
| URL | Country |
|
||||
|-----------------------------------------------|:---------:|
|
||||
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 |
|
||||
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 🇪🇺 |
|
||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 🇪🇺 |
|
||||
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!**
|
||||
|
||||
Thanks to everyone running a public server. **You guys rock!** To the users: Be aware that server operators can log your
|
||||
messages until I finally finish implementing end-to-end encryption.
|
||||
| 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
|
||||
|
||||
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push Notifications that work with just about every platform
|
||||
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
|
||||
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform
|
||||
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
|
||||
- [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
|
||||
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||
- [Scrt.link](https://scrt.link/) - Share a secret
|
||||
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
||||
|
||||
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
|
||||
@@ -42,11 +53,16 @@ messages until I finally finish implementing end-to-end encryption.
|
||||
- [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)
|
||||
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
|
||||
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
||||
|
||||
## CLIs + GUIs
|
||||
|
||||
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
|
||||
- [ntfy Desktop client](https://github.com/mininmobile/ntfy-desktop) - Cross-platform desktop application for ntfy
|
||||
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
|
||||
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
||||
- [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
|
||||
@@ -69,32 +85,77 @@ messages until I finally finish implementing end-to-end encryption.
|
||||
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
||||
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
||||
- [ntfy Discord bot](https://github.com/binwiederhier/ntfy-bot) - ntfy Discord bot (Go)
|
||||
- [ntfy Discord bot](https://github.com/jr1221/ntfy_discord_bot) - An advanced modal-based bot for interacting with the ntfy.sh API (Dart)
|
||||
- [Bettarr Notifications](https://github.com/NiNiyas/Bettarr-Notifications) - Better Notifications for Sonarr and Radarr (Python)
|
||||
- [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)
|
||||
- [aTable/ntfy alertmanager bridge](https://github.com/aTable/ntfy_alertmanager_bridge) - Basic alertmanager bridge to ntfy (JS)
|
||||
- [~xenrox/ntfy-alertmanager](https://hub.xenrox.net/~xenrox/ntfy-alertmanager) - A bridge between ntfy and Alertmanager (Go)
|
||||
- [pinpox/alertmanager-ntfy](https://github.com/pinpox/alertmanager-ntfy) - Relay prometheus alertmanager alerts to ntfy (Go)
|
||||
- [alexbakker/alertmanager-ntfy](https://github.com/alexbakker/alertmanager-ntfy) - Service that forwards Prometheus Alertmanager notifications to ntfy (Go)
|
||||
- [restreamchat2ntfy](https://github.com/kurohuku7/restreamchat2ntfy) - Send restream.io chat to ntfy to check on the Meta Quest (JS)
|
||||
- [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 (Shell)
|
||||
- [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java)
|
||||
- [maubot-ntfy](https://gitlab.com/999eagle/maubot-ntfy) - Matrix bot to subscribe to ntfy topics and send messages to Matrix (Python)
|
||||
- [ntfy-wrapper](https://github.com/vict0rsch/ntfy-wrapper) - Wrapper around ntfy (Python)
|
||||
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
|
||||
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [Self hosted Mobile Push Notifications using NTFY | Thejesh GN](https://thejeshgn.com/2022/08/23/self-hosted-mobile-push-notifications-using-ntfy/) - 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/) - 8/2022
|
||||
- [Docker로 오픈소스 푸시알람 프로젝트 ntfy.sh 설치 및 사용하기.(Feat. Uptimekuma)](https://svrforum.com/svr/398979) - 8/2022
|
||||
- [Easy notifications from R](https://sometimesir.com/posts/easy-notifications-from-r/) - 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/) ⭐ - 6/2022
|
||||
- [无需注册的通知服务ntfy](https://wbsu2003.4everland.app/2022/05/30/%E6%97%A0%E9%9C%80%E6%B3%A8%E5%86%8C%E7%9A%84%E9%80%9A%E7%9F%A5%E6%9C%8D%E5%8A%A1ntfy/) - 5/2022
|
||||
- [Install guide (with Docker)](https://chowdera.com/2022/150/202205301257379077.html) - 5/2022
|
||||
- [Updated review post (Jan-Lukas Else)](https://jlelse.blog/thoughts/2022/04/ntfy) - 4/2022
|
||||
- [Reddit feature update post](https://www.reddit.com/r/selfhosted/comments/uetlso/ntfy_is_a_tool_to_send_push_notifications_to_your/) ⭐ - 4/2022
|
||||
- [無料で簡単に通知の送受信ができつつオープンソースでセルフホストも可能な「ntfy」を使ってみた (Gigazine)](https://gigazine.net/news/20220404-ntfy-push-notification/) - 4/2022
|
||||
- [Pocketmags ntfy review](https://pocketmags.com/us/linux-format-magazine/march-2022/articles/1104187/ntfy) - 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/) ⭐ - 3/2022
|
||||
- [Lemmy post (Jakob)](https://lemmy.eus/post/15541) - 1/2022
|
||||
- [Reddit UnifiedPush release post](https://www.reddit.com/r/selfhosted/comments/s5jylf/my_open_source_notification_android_app_and/) ⭐ - 1/2022
|
||||
- [ntfy: send notifications from your computer to your phone](https://rs1.es/tutorials/2022/01/19/ntfy-send-notifications-phone.html) - 1/2022
|
||||
- [Short ntfy review (Jan-Lukas Else)](https://jlelse.blog/links/2021/12/ntfy-sh) - 12/2021
|
||||
- [Free MacroDroid webhook alternative (FrameXX)](https://www.macrodroidforum.com/index.php?threads/ntfy-sh-free-macrodroid-webhook-alternative.1505/) - 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) - 11/2021
|
||||
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - 12/2021
|
||||
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - 11/2021
|
||||
- [January 2023 Developer Update](https://community.nodebb.org/topic/16908/january-2023-developer-update) - nodebb.org - 1/2023
|
||||
- [Comment envoyer des notifications push sur votre téléphone facilement et gratuitement?](https://korben.info/notifications-push-telephone.html) - 1/2023
|
||||
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
|
||||
- [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
|
||||
- [unRAID Notifications with ntfy.sh](https://lder.dev/posts/ntfy-Notifications-With-unRAID/) - lder.dev - 10/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
|
||||
164
docs/publish.md
164
docs/publish.md
@@ -1316,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
|
||||
```
|
||||
@@ -1326,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"
|
||||
@@ -1335,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'
|
||||
}
|
||||
@@ -1351,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)
|
||||
```
|
||||
@@ -1360,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" })
|
||||
```
|
||||
|
||||
@@ -1379,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.'
|
||||
]
|
||||
]));
|
||||
```
|
||||
@@ -1391,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",
|
||||
@@ -1413,7 +1413,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
}
|
||||
]' \
|
||||
myhome \
|
||||
"Somebody retweetet your tweet."
|
||||
"Somebody retweeted your tweet."
|
||||
```
|
||||
|
||||
=== "HTTP"
|
||||
@@ -1423,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",
|
||||
@@ -1440,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",
|
||||
@@ -1459,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",
|
||||
@@ -1477,7 +1477,7 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
$uri = "https://ntfy.sh"
|
||||
$body = @{
|
||||
topic = "myhome"
|
||||
message = "Somebody retweetet your tweet."
|
||||
message = "Somebody retweeted your tweet."
|
||||
actions = @(
|
||||
@{
|
||||
"action"="view"
|
||||
@@ -1494,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",
|
||||
@@ -1514,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",
|
||||
@@ -2596,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
|
||||
```
|
||||
@@ -2613,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"
|
||||
```
|
||||
@@ -2622,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
|
||||
```
|
||||
@@ -2633,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'
|
||||
}
|
||||
})
|
||||
```
|
||||
@@ -2642,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"
|
||||
@@ -2661,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"
|
||||
})
|
||||
```
|
||||
|
||||
@@ -2672,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
|
||||
|
||||
155
docs/releases.md
155
docs/releases.md
@@ -2,16 +2,155 @@
|
||||
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 server v1.29.0 (UNRELEASED)
|
||||
## ntfy server v1.31.0
|
||||
Released February 14, 2023
|
||||
|
||||
This is a tiny release before the really big release, and also the last before the big v2.0.0. The most interesting
|
||||
things in this release are the new preliminary health endpoint to allow monitoring in K8s (and others), and the removal
|
||||
of `upx` binary packing (which was causing erroneous virus flagging). Aside from that, the `go-smtp` library did a
|
||||
breaking-change upgrade, which required some work to get working again.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Preliminary `/v1/health` API endpoint for service monitoring (no ticket)
|
||||
* Add basic health check to `Dockerfile` ([#555](https://github.com/binwiederhier/ntfy/pull/555), thanks to [@bt90](https://github.com/bt90))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Fix `chown` issues with RHEL-like based systems ([#566](https://github.com/binwiederhier/ntfy/issues/566)/[#565](https://github.com/binwiederhier/ntfy/pull/565), thanks to [@danieldemus](https://github.com/danieldemus))
|
||||
* Removed `upx` (binary packing) for all builds due to false virus warnings ([#576](https://github.com/binwiederhier/ntfy/issues/576), thanks to [@shawnhwei](https://github.com/shawnhwei) for reporting)
|
||||
* Upgraded `go-smtp` library and tests to v0.16.0 ([#569](https://github.com/binwiederhier/ntfy/issues/569))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Add HTTP/2 and TLSv1.3 support to nginx docs ([#553](https://github.com/binwiederhier/ntfy/issues/553), thanks to [@bt90](https://github.com/bt90))
|
||||
* Small wording change for `client.yml` ([#562](https://github.com/binwiederhier/ntfy/pull/562), thanks to [@fleopaulD](https://github.com/fleopaulD))
|
||||
* Fix K8s install docs ([#582](https://github.com/binwiederhier/ntfy/pull/582), thanks to [@Remedan](https://github.com/Remedan))
|
||||
* Updated Jellyseer docs ([#604](https://github.com/binwiederhier/ntfy/pull/604), thanks to [@Y0ngg4n](https://github.com/Y0ngg4n))
|
||||
* Updated iOS developer docs ([#605](https://github.com/binwiederhier/ntfy/pull/605), thanks to [@SticksDev](https://github.com/SticksDev))
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
|
||||
|
||||
## ntfy server v1.30.1
|
||||
Released December 23, 2022 🎅
|
||||
|
||||
This is a special holiday edition version of ntfy, with all sorts of holiday fun and games, and hidden quests.
|
||||
Nahh, just kidding. This release is an intermediate release mainly to eliminate warnings in the logs, so I can
|
||||
roll out the TLSv1.3, HTTP/2 and Unix mode changes on ntfy.sh (see [#552](https://github.com/binwiederhier/ntfy/issues/552)).
|
||||
|
||||
**Features:**
|
||||
|
||||
* Web: Generate random topic name button ([#453](https://github.com/binwiederhier/ntfy/issues/453), thanks to [@yardenshoham](https://github.com/yardenshoham))
|
||||
* Add [Gitpod config](https://github.com/binwiederhier/ntfy/blob/main/.gitpod.yml) ([#540](https://github.com/binwiederhier/ntfy/pull/540), thanks to [@yardenshoham](https://github.com/yardenshoham))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Remove `--env-topic` option from `ntfy publish` as per [deprecation](deprecations.md) (no ticket)
|
||||
* Prepared statements for message cache writes ([#542](https://github.com/binwiederhier/ntfy/pull/542), thanks to [@nicois](https://github.com/nicois))
|
||||
* Do not warn about invalid IP address when behind proxy in unix socket mode (relates to [#552](https://github.com/binwiederhier/ntfy/issues/552))
|
||||
* Upgrade nginx/ntfy config on ntfy.sh to work with TLSv1.3, HTTP/2 ([#552](https://github.com/binwiederhier/ntfy/issues/552), thanks to [@bt90](https://github.com/bt90))
|
||||
|
||||
## ntfy Android app v1.16.0
|
||||
Released December 11, 2022
|
||||
|
||||
This is a feature and platform/dependency upgrade release. You can now have per-subscription notification settings
|
||||
(including sounds, DND, etc.), and you can make notifications continue ringing until they are dismissed. There's also
|
||||
support for thematic/adaptive launcher icon for Android 13.
|
||||
|
||||
There are a few more Android 13 specific things, as well as many bug fixes: No more crashes from large images, no more
|
||||
opening the wrong subscription, and we also fixed the icon color issue.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Custom per-subscription notification settings incl. sounds, DND, etc. ([#6](https://github.com/binwiederhier/ntfy/issues/6), thanks to [@doits](https://github.com/doits))
|
||||
* Insistent notifications that ring until dismissed ([#417](https://github.com/binwiederhier/ntfy/issues/417), thanks to [@danmed](https://github.com/danmed) for reporting)
|
||||
* Add thematic/adaptive launcher icon ([#513](https://github.com/binwiederhier/ntfy/issues/513), thanks to [@daedric7](https://github.com/daedric7) for reporting)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Upgrade Android dependencies and build toolchain to SDK 33 (no ticket)
|
||||
* Simplify F-Droid build: Disable tasks for Google Services ([#516](https://github.com/binwiederhier/ntfy/issues/516), thanks to [@markosopcic](https://github.com/markosopcic))
|
||||
* Android 13: Ask for permission to post notifications ([#508](https://github.com/binwiederhier/ntfy/issues/508))
|
||||
* Android 13: Do not allow swiping away the foreground notification ([#521](https://github.com/binwiederhier/ntfy/issues/521), thanks to [@alexhorner](https://github.com/alexhorner) for reporting)
|
||||
* Android 5 (SDK 21): Fix crash on unsubscribing ([#528](https://github.com/binwiederhier/ntfy/issues/528), thanks to Roger M.)
|
||||
* Remove timestamp when copying message text ([#471](https://github.com/binwiederhier/ntfy/issues/471), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Fix auto-delete if some icons do not exist anymore ([#506](https://github.com/binwiederhier/ntfy/issues/506))
|
||||
* Fix notification icon color ([#480](https://github.com/binwiederhier/ntfy/issues/480), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d) for reporting)
|
||||
* Fix topics do not re-subscribe to Firebase after restoring from backup ([#511](https://github.com/binwiederhier/ntfy/issues/511))
|
||||
* Fix crashes from large images ([#474](https://github.com/binwiederhier/ntfy/issues/474), thanks to [@daedric7](https://github.com/daedric7) for reporting)
|
||||
* Fix notification click opens wrong subscription ([#261](https://github.com/binwiederhier/ntfy/issues/261), thanks to [@SMAW](https://github.com/SMAW) for reporting)
|
||||
* Fix Firebase-only "link expired" issue ([#529](https://github.com/binwiederhier/ntfy/issues/529))
|
||||
* Remove "Install .apk" feature in Google Play variant due to policy change ([#531](https://github.com/binwiederhier/ntfy/issues/531))
|
||||
* Add donate button (no ticket)
|
||||
|
||||
**Additional translations:**
|
||||
|
||||
* 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:**
|
||||
|
||||
@@ -21,19 +160,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
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!
|
||||
You guys rock!
|
||||
|
||||
Sponsors (alphabetical order):
|
||||
|
||||
* [@aspyct](https://github.com/aspyct)
|
||||
* [@codinghipster](https://github.com/codinghipster)
|
||||
* [@HinFort](https://github.com/HinFort)
|
||||
* [@mckay115](https://github.com/mckay115)
|
||||
* [@neutralinsomniac](https://github.com/neutralinsomniac)
|
||||
* [@nickexyz](https://github.com/nickexyz)
|
||||
* [@qcasey](https://github.com/qcasey)
|
||||
* [@Salamafet](https://github.com/Salamafet)
|
||||
* +1 private sponsor
|
||||
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
|
||||
|
||||
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 |
@@ -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
|
||||
|
||||
56
go.mod
56
go.mod
@@ -3,55 +3,59 @@ module heckel.io/ntfy
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.6.1 // indirect
|
||||
cloud.google.com/go/storage v1.27.0 // indirect
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||
cloud.google.com/go/storage v1.29.0 // 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/emersion/go-smtp v0.16.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.17.1
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
|
||||
google.golang.org/api v0.98.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.24.3
|
||||
golang.org/x/crypto v0.6.0
|
||||
golang.org/x/oauth2 v0.5.0 // indirect
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.5.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/api v0.110.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.104.0 // indirect
|
||||
cloud.google.com/go/compute v1.10.0 // indirect
|
||||
cloud.google.com/go/iam v0.5.0 // indirect
|
||||
cloud.google.com/go v0.109.0 // indirect
|
||||
cloud.google.com/go/compute v1.18.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.10.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.4.1 // 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-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.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // 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-20220930213112-107f3e3c3b0b // indirect
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.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.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
|
||||
google.golang.org/grpc v1.53.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
788
go.sum
788
go.sum
@@ -1,164 +1,30 @@
|
||||
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/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
|
||||
cloud.google.com/go v0.104.0 h1:gSmWO7DY1vOm0MVU6DNXM11BWHHsTUmsC5cv1fuW5X8=
|
||||
cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
|
||||
cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
|
||||
cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
|
||||
cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
|
||||
cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
|
||||
cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
|
||||
cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
|
||||
cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
|
||||
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/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
|
||||
cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
|
||||
cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
|
||||
cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
|
||||
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/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
|
||||
cloud.google.com/go/compute v1.10.0 h1:aoLIYaA1fX3ywihqpBk2APQKOo20nXsp1GEZQbx5Jk4=
|
||||
cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
|
||||
cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
|
||||
cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
|
||||
cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
|
||||
cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
|
||||
cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
|
||||
cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
|
||||
cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
|
||||
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/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
|
||||
cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
|
||||
cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
|
||||
cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
|
||||
cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
|
||||
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/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
|
||||
cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
|
||||
cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
|
||||
cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
|
||||
cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
|
||||
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
|
||||
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
|
||||
cloud.google.com/go/iam v0.4.0 h1:YBYU00SCDzZJdHqVc4I5d6lsklcYIjQZa1YmEz4jlSE=
|
||||
cloud.google.com/go/iam v0.4.0/go.mod h1:cbaZxyScUhxl7ZAkNWiALgihfP75wS/fUsVNaa1r3vA=
|
||||
cloud.google.com/go/iam v0.5.0 h1:fz9X5zyTWBmamZsqvqZqD7khbifcZF/q+Z1J8pfhIUg=
|
||||
cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
|
||||
cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
|
||||
cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
|
||||
cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
|
||||
cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
|
||||
cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
|
||||
cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
|
||||
cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
|
||||
cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
|
||||
cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
|
||||
cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
|
||||
cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
|
||||
cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
|
||||
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/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
|
||||
cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
|
||||
cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
|
||||
cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
|
||||
cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
|
||||
cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
|
||||
cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
|
||||
cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
|
||||
cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
|
||||
cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
|
||||
cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
|
||||
cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
|
||||
cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
|
||||
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/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
|
||||
cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ=
|
||||
cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s=
|
||||
cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
|
||||
cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
|
||||
cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
|
||||
cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
|
||||
cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
|
||||
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
|
||||
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=
|
||||
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
||||
cloud.google.com/go v0.109.0 h1:38CZoKGlCnPZjGdyj0ZfpoGae0/wgNfy5F0byyxg0Gk=
|
||||
cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE=
|
||||
cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
|
||||
cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
|
||||
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.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
|
||||
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
|
||||
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||
cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
|
||||
firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A=
|
||||
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/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
|
||||
github.com/BurntSushi/toml v1.2.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.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=
|
||||
@@ -167,659 +33,152 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-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/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.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.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||
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/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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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 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/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/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/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/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||
github.com/googleapis/gax-go/v2 v2.5.1 h1:kBRZU0PSuI7PspsSb/ChWoVResUcwNVIdpB049pKTiw=
|
||||
github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||
github.com/googleapis/gax-go/v2 v2.7.0/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.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
|
||||
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||
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/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.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
|
||||
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
github.com/urfave/cli/v2 v2.17.1 h1:UzjDEw2dJQUE3iRaiNQ1VrVFbyAtKGH3VdkMoHA58V0=
|
||||
github.com/urfave/cli/v2 v2.17.1/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
|
||||
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.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.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
|
||||
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/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-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-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-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-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20220927155233-aa73b2587036 h1:GDWXwjBkdo4XMin5T4iul98eH4BfGOR7TucJ057FxjY=
|
||||
golang.org/x/net v0.0.0-20220927155233-aa73b2587036/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b h1:uKO3Js8lXGjpjdc4J3rqs0/Ex5yDKUGfk43tTYWVLas=
|
||||
golang.org/x/net v0.0.0-20220930213112-107f3e3c3b0b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-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/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 h1:lxqLZaMad/dJHMFZH0NiNpiEZI/nhgWhe4wgzpE+MuA=
|
||||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
|
||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/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-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25 h1:nwzwVf0l2Y/lkov/+IYgMMbFyI+QypZDds9RxlSmsFQ=
|
||||
golang.org/x/sys v0.0.0-20220926163933-8cfa568d3c25/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/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.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.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-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.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/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
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.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.77.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/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
|
||||
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
|
||||
google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
|
||||
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ=
|
||||
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.98.0 h1:yxZrcxXESimy6r6mdL5Q6EnZwmewDJK2dVg3g75s5Dg=
|
||||
google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
|
||||
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.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/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-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
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-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
|
||||
google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
|
||||
google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
|
||||
google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
|
||||
google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
|
||||
google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
|
||||
google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
|
||||
google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
|
||||
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
|
||||
google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704 h1:H1AcWFV69NFCMeBJ8nVLtv8uHZZ5Ozcgoq012hHEFuU=
|
||||
google.golang.org/genproto v0.0.0-20220927151529-dcaddaf36704/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
|
||||
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 h1:Ezh2cpcnP5Rq60sLensUsFnxh7P6513NLvNtCm9iyJ4=
|
||||
google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
|
||||
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
|
||||
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.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/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
|
||||
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -828,32 +187,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/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=
|
||||
|
||||
@@ -87,8 +87,9 @@ nav:
|
||||
- "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
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ set -e
|
||||
if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
||||
if [ -d /run/systemd/system ]; then
|
||||
# Create ntfy user/group
|
||||
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home ntfy
|
||||
chown ntfy.ntfy /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
||||
groupadd -f ntfy
|
||||
id ntfy >/dev/null 2>&1 || useradd --system --no-create-home -g ntfy ntfy
|
||||
chown ntfy:ntfy /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
||||
chmod 700 /var/cache/ntfy /var/cache/ntfy/attachments /var/lib/ntfy
|
||||
|
||||
# Hack to change permissions on cache file
|
||||
@@ -16,7 +17,7 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
||||
if [ -f "$configfile" ]; then
|
||||
cachefile="$(cat "$configfile" | perl -n -e'/^\s*cache-file: ["'"'"']?([^"'"'"']+)["'"'"']?/ && print $1')" # Oh my, see #47
|
||||
if [ -n "$cachefile" ]; then
|
||||
chown ntfy.ntfy "$cachefile" || true
|
||||
chown ntfy:ntfy "$cachefile" || true
|
||||
chmod 600 "$cachefile" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -60,6 +61,8 @@ type Config struct {
|
||||
CacheFile string
|
||||
CacheDuration time.Duration
|
||||
CacheStartupQueries string
|
||||
CacheBatchSize int
|
||||
CacheBatchTimeout time.Duration
|
||||
AuthFile string
|
||||
AuthDefaultRead bool
|
||||
AuthDefaultWrite bool
|
||||
@@ -92,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
|
||||
@@ -113,6 +116,8 @@ func NewConfig() *Config {
|
||||
FirebaseKeyFile: "",
|
||||
CacheFile: "",
|
||||
CacheDuration: DefaultCacheDuration,
|
||||
CacheBatchSize: 0,
|
||||
CacheBatchTimeout: 0,
|
||||
AuthFile: "",
|
||||
AuthDefaultRead: true,
|
||||
AuthDefaultWrite: true,
|
||||
@@ -135,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,
|
||||
|
||||
@@ -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 (
|
||||
@@ -42,6 +44,7 @@ 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;
|
||||
`
|
||||
@@ -90,7 +93,7 @@ const (
|
||||
|
||||
// Schema management queries
|
||||
const (
|
||||
currentSchemaVersion = 8
|
||||
currentSchemaVersion = 9
|
||||
createSchemaVersionTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||
id INT PRIMARY KEY,
|
||||
@@ -183,15 +186,21 @@ const (
|
||||
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
|
||||
@@ -199,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.
|
||||
@@ -226,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
|
||||
@@ -262,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,
|
||||
@@ -279,7 +315,7 @@ func (c *messageCache) addMessages(ms []*message) error {
|
||||
attachmentSize,
|
||||
attachmentExpires,
|
||||
attachmentURL,
|
||||
m.Sender,
|
||||
sender,
|
||||
m.Encoding,
|
||||
published,
|
||||
)
|
||||
@@ -287,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) {
|
||||
@@ -393,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) {
|
||||
@@ -415,6 +460,17 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) {
|
||||
return size, nil
|
||||
}
|
||||
|
||||
func (c *messageCache) processMessageBatches() {
|
||||
if c.queue == nil {
|
||||
return
|
||||
}
|
||||
for messages := range c.queue.Dequeue() {
|
||||
if err := c.addMessages(messages); err != nil {
|
||||
log.Error("Cache: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
defer rows.Close()
|
||||
messages := make([]*message, 0)
|
||||
@@ -454,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{
|
||||
@@ -477,7 +537,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||
Icon: icon,
|
||||
Actions: actions,
|
||||
Attachment: att,
|
||||
Sender: sender,
|
||||
Sender: senderIP, // Must parse assuming database must be correct
|
||||
Encoding: encoding,
|
||||
})
|
||||
}
|
||||
@@ -535,6 +595,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error {
|
||||
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)
|
||||
}
|
||||
@@ -640,5 +702,16 @@ func migrateFrom7(db *sql.DB) error {
|
||||
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)
|
||||
@@ -444,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)
|
||||
@@ -455,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)
|
||||
@@ -466,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)
|
||||
@@ -495,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)
|
||||
}
|
||||
@@ -507,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
|
||||
@@ -67,6 +68,7 @@ var (
|
||||
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
|
||||
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
|
||||
|
||||
healthPath = "/v1/health"
|
||||
webConfigPath = "/config.js"
|
||||
userStatsPath = "/user/stats"
|
||||
matrixPushPath = "/_matrix/push/v1/notify"
|
||||
@@ -150,7 +152,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 +160,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()
|
||||
}
|
||||
@@ -295,6 +297,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == healthPath {
|
||||
return s.handleHealth(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
||||
@@ -365,6 +369,18 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||
response := &apiHealthResponse{
|
||||
Healthy: true,
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||
appRoot := "/"
|
||||
if !s.config.WebRootIsApp {
|
||||
@@ -490,6 +506,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
|
||||
}
|
||||
@@ -1219,7 +1236,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):
|
||||
@@ -1286,7 +1303,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.Contains(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
|
||||
@@ -1436,21 +1453,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]
|
||||
|
||||
@@ -53,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
|
||||
@@ -65,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.
|
||||
@@ -173,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"
|
||||
|
||||
@@ -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 {
|
||||
@@ -322,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()))
|
||||
|
||||
@@ -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) {
|
||||
@@ -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"
|
||||
@@ -292,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) {
|
||||
@@ -814,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)
|
||||
@@ -834,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)
|
||||
}
|
||||
@@ -1132,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
|
||||
@@ -1168,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")
|
||||
@@ -1195,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")
|
||||
@@ -1216,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) {
|
||||
@@ -1391,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) {
|
||||
@@ -1402,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) {
|
||||
@@ -1413,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
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ type smtpBackend struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ smtp.Backend = (*smtpBackend)(nil)
|
||||
var _ smtp.Session = (*smtpSession)(nil)
|
||||
|
||||
func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend {
|
||||
return &smtpBackend{
|
||||
config: conf,
|
||||
@@ -41,14 +44,9 @@ func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
||||
log.Debug("%s Incoming mail, login with user %s", logSMTPPrefix(state), username)
|
||||
return &smtpSession{backend: b, state: state}, nil
|
||||
}
|
||||
|
||||
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||
log.Debug("%s Incoming mail, anonymous login", logSMTPPrefix(state))
|
||||
return &smtpSession{backend: b, state: state}, nil
|
||||
func (b *smtpBackend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
||||
log.Debug("%s Incoming mail", logSMTPPrefix(conn))
|
||||
return &smtpSession{backend: b, conn: conn}, nil
|
||||
}
|
||||
|
||||
func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
|
||||
@@ -60,23 +58,23 @@ func (b *smtpBackend) Counts() (total int64, success int64, failure int64) {
|
||||
// smtpSession is returned after EHLO.
|
||||
type smtpSession struct {
|
||||
backend *smtpBackend
|
||||
state *smtp.ConnectionState
|
||||
conn *smtp.Conn
|
||||
topic string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
||||
log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.state), username)
|
||||
func (s *smtpSession) AuthPlain(username, _ string) error {
|
||||
log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.conn), username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
||||
log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.state), from, opts)
|
||||
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
|
||||
log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.conn), from, opts)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *smtpSession) Rcpt(to string) error {
|
||||
log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.state), to)
|
||||
log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.conn), to)
|
||||
return s.withFailCount(func() error {
|
||||
conf := s.backend.config
|
||||
addressList, err := mail.ParseAddressList(to)
|
||||
@@ -114,9 +112,9 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
if log.IsTrace() {
|
||||
log.Trace("%s DATA: %s", logSMTPPrefix(s.state), string(b))
|
||||
log.Trace("%s DATA: %s", logSMTPPrefix(s.conn), string(b))
|
||||
} else if log.IsDebug() {
|
||||
log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.state), len(b))
|
||||
log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.conn), len(b))
|
||||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
@@ -156,9 +154,9 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
|
||||
func (s *smtpSession) publishMessage(m *message) error {
|
||||
// Extract remote address (for rate limiting)
|
||||
remoteAddr, _, err := net.SplitHostPort(s.state.RemoteAddr.String())
|
||||
remoteAddr, _, err := net.SplitHostPort(s.conn.Conn().RemoteAddr().String())
|
||||
if err != nil {
|
||||
remoteAddr = s.state.RemoteAddr.String()
|
||||
remoteAddr = s.conn.Conn().RemoteAddr().String()
|
||||
}
|
||||
|
||||
// Call HTTP handler with fake HTTP request
|
||||
@@ -198,7 +196,7 @@ func (s *smtpSession) withFailCount(fn func() error) error {
|
||||
if err != nil {
|
||||
// Almost all of these errors are parse errors, and user input errors.
|
||||
// We do not want to spam the log with WARN messages.
|
||||
log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.state), err.Error())
|
||||
log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.conn), err.Error())
|
||||
s.backend.failure++
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSmtpBackend_Multipart(t *testing.T) {
|
||||
email := `MIME-Version: 1.0
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||
Subject: and one more
|
||||
@@ -28,20 +35,25 @@ Content-Type: text/html; charset="UTF-8"
|
||||
|
||||
<div dir="ltr">what's up<br clear="all"><div><br></div></div>
|
||||
|
||||
--000000000000f3320b05d42915c9--`
|
||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
--000000000000f3320b05d42915c9--
|
||||
.
|
||||
`
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "and one more", r.Header.Get("Title"))
|
||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||
})
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_MultipartNoBody(t *testing.T) {
|
||||
email := `MIME-Version: 1.0
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: ntfy-emailtest@ntfy.sh
|
||||
DATA
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 28 Dec 2021 01:33:34 +0100
|
||||
Message-ID: <CAAvm7ABCDsi9vsuu0WTRXzZQBC8dXrDOLT8iCWdqrsmg@mail.gmail.com>
|
||||
Subject: This email has a subject but no body
|
||||
@@ -59,20 +71,25 @@ Content-Type: text/html; charset="UTF-8"
|
||||
|
||||
<div dir="ltr"><br></div>
|
||||
|
||||
--000000000000bcf4a405d429f8d4--`
|
||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
--000000000000bcf4a405d429f8d4--
|
||||
.
|
||||
`
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/emailtest", r.URL.Path)
|
||||
require.Equal(t, "", r.Header.Get("Title")) // We flipped message and body
|
||||
require.Equal(t, "This email has a subject but no body", readAll(t, r.Body))
|
||||
})
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("ntfy-emailtest@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Plaintext(t *testing.T) {
|
||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: mytopic@ntfy.sh
|
||||
DATA
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||
Subject: and one more
|
||||
From: Phil <phil@example.com>
|
||||
@@ -80,56 +97,68 @@ To: mytopic@ntfy.sh
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
what's up
|
||||
.
|
||||
`
|
||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "and one more", r.Header.Get("Title"))
|
||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||
})
|
||||
conf.SMTPServerAddrPrefix = ""
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
|
||||
email := `Subject: Very short mail
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: mytopic@ntfy.sh
|
||||
DATA
|
||||
Subject: Very short mail
|
||||
|
||||
what's up
|
||||
.
|
||||
`
|
||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Very short mail", r.Header.Get("Title"))
|
||||
require.Equal(t, "what's up", readAll(t, r.Body))
|
||||
})
|
||||
conf.SMTPServerAddrPrefix = ""
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
|
||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
|
||||
From: Phil <phil@example.com>
|
||||
To: ntfy-mytopic@ntfy.sh
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
what's up
|
||||
.
|
||||
`
|
||||
_, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "Three santas 🎅🎅🎅", r.Header.Get("Title"))
|
||||
})
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Plaintext_TooLongTruncate(t *testing.T) {
|
||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: mytopic@ntfy.sh
|
||||
DATA
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||
Subject: and one more
|
||||
From: Phil <phil@example.com>
|
||||
@@ -148,60 +177,61 @@ so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
that should do it
|
||||
.
|
||||
`
|
||||
conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
expected := `you know this is a string.
|
||||
it's a long string.
|
||||
it's supposed to be longer than the max message length
|
||||
@@ -214,68 +244,71 @@ so i'm gonna fill the rest of this with AAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
......................................................................
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp
|
||||
and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
BBBBBBBBBBBBBBBBBBBBBBBBB`
|
||||
require.Equal(t, 4096, len(expected)) // Sanity check
|
||||
require.Equal(t, expected, readAll(t, r.Body))
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
conf.SMTPServerAddrPrefix = ""
|
||||
session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4"))
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Unsupported(t *testing.T) {
|
||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Message-ID: <CAAvm79YP0C=Rt1N=KWmSUBB87KK2rRChmdzKqF1vCwMEUiVzLQ@mail.gmail.com>
|
||||
Subject: and one more
|
||||
From: Phil <phil@example.com>
|
||||
@@ -283,34 +316,89 @@ To: mytopic@ntfy.sh
|
||||
Content-Type: text/SOMETHINGELSE
|
||||
|
||||
what's up
|
||||
.
|
||||
`
|
||||
conf, backend := newTestBackend(t, func(http.ResponseWriter, *http.Request) {
|
||||
// Nothing.
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("This should not be called")
|
||||
})
|
||||
conf.SMTPServerAddrPrefix = ""
|
||||
session, _ := backend.Login(fakeConnState(t, "1.2.3.4"), "user", "pass")
|
||||
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
||||
require.Equal(t, errUnsupportedContentType, session.Data(strings.NewReader(email)))
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: unsupported content type")
|
||||
}
|
||||
|
||||
func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*Config, *smtpBackend) {
|
||||
conf := newTestConfig(t)
|
||||
func TestSmtpBackend_InvalidAddress(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
RCPT TO: unsupported@ntfy.sh
|
||||
DATA
|
||||
Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||
Subject: and one more
|
||||
From: Phil <phil@example.com>
|
||||
To: mytopic@ntfy.sh
|
||||
Content-Type: text/plain
|
||||
|
||||
what's up
|
||||
.
|
||||
`
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("This should not be called")
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "451 4.0.0 invalid address")
|
||||
}
|
||||
|
||||
type smtpHandlerFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) {
|
||||
conf = newTestConfig(t)
|
||||
conf.SMTPServerListen = ":25"
|
||||
conf.SMTPServerDomain = "ntfy.sh"
|
||||
conf.SMTPServerAddrPrefix = "ntfy-"
|
||||
backend := newMailBackend(conf, handler)
|
||||
return conf, backend
|
||||
}
|
||||
|
||||
func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState {
|
||||
ip, err := net.ResolveIPAddr("ip", remoteAddr)
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &smtp.ConnectionState{
|
||||
Hostname: "myhostname",
|
||||
LocalAddr: ip,
|
||||
RemoteAddr: ip,
|
||||
s = smtp.NewServer(backend)
|
||||
s.Domain = conf.SMTPServerDomain
|
||||
s.AllowInsecureAuth = true
|
||||
go func() {
|
||||
require.Nil(t, s.Serve(l))
|
||||
}()
|
||||
c, err = net.Dial("tcp", l.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
scanner = bufio.NewScanner(c)
|
||||
return
|
||||
}
|
||||
|
||||
func writeAndReadUntilLine(t *testing.T, email string, conn net.Conn, scanner *bufio.Scanner, expectedLine string) {
|
||||
_, err := io.WriteString(conn, email)
|
||||
require.Nil(t, err)
|
||||
readUntilLine(t, conn, scanner, expectedLine)
|
||||
}
|
||||
|
||||
func readUntilLine(t *testing.T, conn net.Conn, scanner *bufio.Scanner, expectedLine string) {
|
||||
cancelChan := make(chan bool)
|
||||
go func() {
|
||||
select {
|
||||
case <-cancelChan:
|
||||
case <-time.After(3 * time.Second):
|
||||
conn.Close()
|
||||
t.Error("Failed waiting for expected output")
|
||||
}
|
||||
}()
|
||||
var output string
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
if strings.TrimSpace(text) == expectedLine {
|
||||
cancelChan <- true
|
||||
return
|
||||
}
|
||||
output += text + "\n"
|
||||
//fmt.Println(text)
|
||||
}
|
||||
t.Fatalf("Expected line '%s' not found in output:\n%s", expectedLine, output)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"heckel.io/ntfy/util"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
// List of possible events
|
||||
@@ -33,7 +35,7 @@ type message struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -211,3 +213,7 @@ func (q *queryFilter) Pass(msg *message) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type apiHealthResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ func logHTTPPrefix(v *visitor, r *http.Request) string {
|
||||
return fmt.Sprintf("%s HTTP %s %s", v.ip, r.Method, requestURI)
|
||||
}
|
||||
|
||||
func logSMTPPrefix(state *smtp.ConnectionState) string {
|
||||
return fmt.Sprintf("%s/%s SMTP", state.Hostname, state.RemoteAddr.String())
|
||||
func logSMTPPrefix(conn *smtp.Conn) string {
|
||||
return fmt.Sprintf("%s/%s SMTP", conn.Hostname(), conn.Conn().RemoteAddr().String())
|
||||
}
|
||||
|
||||
func renderHTTPRequest(r *http.Request) string {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
16
util/util.go
16
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 (
|
||||
@@ -45,6 +47,16 @@ func Contains[T comparable](haystack []T, needle T) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
@@ -42,6 +44,13 @@ func TestContains(t *testing.T) {
|
||||
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) {
|
||||
require.Equal(t, []string{}, SplitNoEmpty("", ","))
|
||||
require.Equal(t, []string{}, SplitNoEmpty(",,,", ","))
|
||||
|
||||
14583
web/package-lock.json
generated
14583
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": "Поверително разглеждане не се поддържа",
|
||||
|
||||
@@ -129,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",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"message_bar_type_message": "Tapez un message ici",
|
||||
"notifications_attachment_open_button": "Ouvrir la pièce jointe",
|
||||
"notifications_attachment_link_expires": "le lien expire {{date}}",
|
||||
"message_bar_error_publishing": "Notification d'erreur de publication",
|
||||
"message_bar_error_publishing": "Erreur lors de la publication de la notification",
|
||||
"nav_button_all_notifications": "Toutes les notifications",
|
||||
"nav_button_settings": "Paramètres",
|
||||
"nav_button_documentation": "Documentation",
|
||||
@@ -80,7 +80,7 @@
|
||||
"subscribe_dialog_login_title": "Connexion nécessaire",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Priorité basse et au-dessus",
|
||||
"prefs_users_dialog_button_cancel": "Annuler",
|
||||
"error_boundary_button_copy_stack_trace": "Copier la stack strace",
|
||||
"error_boundary_button_copy_stack_trace": "Copier la trace d'appels",
|
||||
"publish_dialog_attached_file_title": "Fichier joint :",
|
||||
"publish_dialog_checkbox_publish_another": "Publier un autre",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Nom du fichier joint",
|
||||
@@ -129,7 +129,7 @@
|
||||
"prefs_users_table_user_header": "Utilisateur",
|
||||
"prefs_users_dialog_title_edit": "Éditer l'utilisateur",
|
||||
"prefs_users_dialog_button_add": "Ajouter",
|
||||
"error_boundary_description": "Ceci ne devrait évidemment pas arriver. Désolé pour ça.<br/>Si vous avez une minute, merci de <githubLink>signaler ceci sur GitHub</githubLink>, ou faites-le nous savoir par <discordLink>Discord</discordLink> ou <matrixLink>Matric</matrixLink>.",
|
||||
"error_boundary_description": "Ceci ne devrait évidemment pas arriver. Désolé pour ça.<br/>Si vous avez une minute, merci de <githubLink>signaler ceci sur GitHub</githubLink>, ou faites-le nous savoir par <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||
"prefs_users_dialog_title_add": "Ajouter un utilisateur",
|
||||
"error_boundary_stack_trace": "Trace de pile d'appels",
|
||||
"error_boundary_gathering_info": "Récupérer plus d'information…",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"publish_dialog_topic_label": "Emnenavn",
|
||||
"prefs_notifications_delete_after_one_day_description": "Merknader slettes automatisk etter én dag",
|
||||
"notifications_click_copy_url_button": "Kopier lenke",
|
||||
"error_boundary_title": "Oida. Ntfy krasjet.",
|
||||
"error_boundary_title": "Oida, ntfy krasjet",
|
||||
"publish_dialog_message_placeholder": "Skriv en melding her",
|
||||
"publish_dialog_button_cancel": "Avbryt",
|
||||
"prefs_notifications_min_priority_title": "Minimumsprioritet",
|
||||
@@ -118,9 +118,74 @@
|
||||
"prefs_users_table_base_url_header": "Tjeneste-nettadresse",
|
||||
"prefs_users_dialog_button_cancel": "Avbryt",
|
||||
"prefs_users_dialog_button_add": "Legg til",
|
||||
"publish_dialog_chip_attach_url_label": "Legg ved fil per nettadresse",
|
||||
"publish_dialog_chip_attach_url_label": "Legg til fil med nettadresse",
|
||||
"publish_dialog_tags_placeholder": "Kommainndelt liste over etiketter, f.eks. advarsel, srv1-sikkerhetskopi",
|
||||
"prefs_notifications_sound_description_none": "Merknader er lydløse når de mottas",
|
||||
"prefs_notifications_sound_description_none": "Merknader spiller ikke lyd når de mottas",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_varsler",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Forvalgt prioritet og høyere"
|
||||
"prefs_notifications_min_priority_default_and_higher": "Forvalgt prioritet og høyere",
|
||||
"notifications_no_subscriptions_title": "Det ser ut til at du ikke har noen abonnementer ennå.",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "overskrider {{fileSizeLimit}} filgrense og kvote, {{remainingBytes}} gjenstår",
|
||||
"publish_dialog_attachment_limits_file_reached": "overskrider filgrensen på {{fileSizeLimit}}",
|
||||
"publish_dialog_title_label": "Tittel",
|
||||
"publish_dialog_title_placeholder": "Varslingstittel, f.eks. Diskplassvarsel",
|
||||
"publish_dialog_topic_placeholder": "Emnenavn, f.eks. halgeir_varsler",
|
||||
"publish_dialog_chip_click_label": "Klikk URL",
|
||||
"publish_dialog_chip_delay_label": "Forsink leveringen",
|
||||
"publish_dialog_details_examples_description": "For eksempler og en detaljert beskrivelse av alle sendefunksjoner, se <docsLink>dokumentasjonen</docsLink>.",
|
||||
"publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com",
|
||||
"alert_grant_description": "Gi nettleseren din tillatelse til å vise skrivebordsvarsler.",
|
||||
"alert_not_supported_description": "Varsler støttes ikke i nettleseren din.",
|
||||
"notifications_attachment_file_app": "Android-app-fil",
|
||||
"notifications_no_subscriptions_description": "Klikk på \"{{linktext}}\"-koblingen for å opprette eller abonnere på et emne. Etter det kan du sende meldinger via PUT eller POST, og du vil motta varsler her.",
|
||||
"notifications_actions_http_request_title": "Send HTTP {{metode}} til {{url}}",
|
||||
"notifications_none_for_any_description": "For å sende varsler til et emne, bare PUT eller POST til emne-URLen. Her er et eksempel som bruker et av emnene dine.",
|
||||
"notifications_more_details": "For mer informasjon, sjekk ut <websiteLink>nettstedet</websiteLink> eller <docsLink>dokumentasjonen</docsLink>.",
|
||||
"publish_dialog_attachment_limits_quota_reached": "overskrider kvoten, {{remainingBytes}} gjenstår",
|
||||
"publish_dialog_click_reset": "Fjern klikk-URL",
|
||||
"publish_dialog_delay_placeholder": "Forsinket levering, f.eks. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (bare på engelsk)",
|
||||
"emoji_picker_search_clear": "Tøm søk",
|
||||
"subscribe_dialog_subscribe_description": "Det kan hende emner ikke er passordsbeskyttet, så velg et navn som ikke er enkelt å gjette. Når du har abonnert kan du utføre PUT/POST av merknader.",
|
||||
"publish_dialog_checkbox_publish_another": "Publiser enda en",
|
||||
"subscribe_dialog_login_description": "Dette emnet er passordbeskyttet. Vennligst skriv inn brukernavn og passord for å abonnere.",
|
||||
"prefs_notifications_sound_play": "Spill av valgt lyd",
|
||||
"subscribe_dialog_error_user_not_authorized": "Bruker {{brukernavn}} ikke autorisert",
|
||||
"prefs_users_delete_button": "Slett bruker",
|
||||
"error_boundary_unsupported_indexeddb_description": "ntfy-nettappen trenger IndexedDB for å fungere, og nettleseren din støtter ikke IndexedDB i privat nettlesingsmodus.<br/><br/>Selv om dette er uheldig, gir det heller ikke så mye mening å bruke ntfy-nettappen i privat surfemodus uansett, fordi alt er lagret i nettleserlagringen. Du kan lese mer om det <githubLink>i denne GitHub-feilmeldingen</githubLink>, eller snakk med oss på <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
|
||||
"action_bar_show_menu": "Vis meny",
|
||||
"action_bar_toggle_mute": "Aktiver/deaktiver notifikasjoner",
|
||||
"prefs_notifications_min_priority_description_max": "Vis merknader hvis prioritet er 5 (maks.)",
|
||||
"prefs_notifications_min_priority_any": "Hvilken som helst prioritet",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Lav prioritet og høyere",
|
||||
"prefs_users_description": "Legg til/fjern brukere for dine beskyttede emner her. Vær oppmerksom på at brukernavn og passord er lagret i nettleserens lokale lagring.",
|
||||
"error_boundary_description": "Dette skal åpenbart ikke skje. Beklager dette.<br/>Hvis du har et minutt, vennligst <githubLink>rapporter dette på GitHub</githubLink>, eller gi oss beskjed via <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
|
||||
"action_bar_logo_alt": "ntfy logo",
|
||||
"message_bar_publish": "Publiser melding",
|
||||
"action_bar_toggle_action_menu": "Åpne/lukk handlingsmeny",
|
||||
"message_bar_show_dialog": "Vis publiseringsdialog",
|
||||
"nav_button_muted": "Varsler dempet",
|
||||
"nav_button_connecting": "kobler til",
|
||||
"notifications_list": "Varslingsliste",
|
||||
"notifications_list_item": "Varsling",
|
||||
"notifications_mark_read": "Merk som lest",
|
||||
"notifications_delete": "Slett",
|
||||
"notifications_priority_x": "Prioritet {{prioritet}}",
|
||||
"notifications_new_indicator": "Nytt varsel",
|
||||
"notifications_attachment_image": "Vedlagt bilde",
|
||||
"notifications_attachment_file_image": "bildefil",
|
||||
"notifications_attachment_file_video": "videofil",
|
||||
"notifications_attachment_file_audio": "lydfil",
|
||||
"notifications_attachment_file_document": "annet dokument",
|
||||
"notifications_actions_not_supported": "Handling støttes ikke i nettappen",
|
||||
"notifications_none_for_topic_description": "For å sende varsler til dette emnet, bare PUT eller POST til emne-URLen.",
|
||||
"publish_dialog_emoji_picker_show": "Velg emoji",
|
||||
"publish_dialog_topic_reset": "Tilbakestill emne",
|
||||
"publish_dialog_click_label": "Klikk URL",
|
||||
"publish_dialog_email_reset": "Fjern videresending av e-post",
|
||||
"publish_dialog_attach_reset": "Fjern URL vedlegg",
|
||||
"publish_dialog_delay_reset": "Fjern forsinket levering",
|
||||
"publish_dialog_attached_file_remove": "Fjern vedlagt fil",
|
||||
"subscribe_dialog_subscribe_base_url_label": "Tjeneste-URL",
|
||||
"prefs_users_table": "Brukertabell",
|
||||
"prefs_users_edit_button": "Rediger bruker",
|
||||
"error_boundary_unsupported_indexeddb_title": "Privat surfing støttes ikke"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"action_bar_settings": "Instellingen",
|
||||
"action_bar_send_test_notification": "Stuur test notificatie",
|
||||
"action_bar_send_test_notification": "Verstuur testnotificatie.",
|
||||
"action_bar_clear_notifications": "Wis alle notificaties",
|
||||
"message_bar_type_message": "Typ hier een bericht",
|
||||
"action_bar_unsubscribe": "Afmelden",
|
||||
"message_bar_error_publishing": "Fout bij publiceren notificatie",
|
||||
"nav_topics_title": "Geabonneerde onderwerpen",
|
||||
"nav_button_settings": "Instellingen",
|
||||
"alert_not_supported_description": "Notificaties worden niet ondersteund in je browser.",
|
||||
"alert_not_supported_description": "Notificaties worden niet ondersteund door je browser.",
|
||||
"notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.",
|
||||
"publish_dialog_tags_label": "Tags",
|
||||
"publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen",
|
||||
@@ -26,20 +26,20 @@
|
||||
"action_bar_show_menu": "Toon menu",
|
||||
"action_bar_logo_alt": "ntfy logo",
|
||||
"action_bar_toggle_mute": "Notificaties dempen/opheffen",
|
||||
"action_bar_toggle_action_menu": "Actie menu openen/sluiten",
|
||||
"action_bar_toggle_action_menu": "Open/Sluit actiemenu",
|
||||
"message_bar_show_dialog": "Toon publicatie venster",
|
||||
"message_bar_publish": "Bericht publiceren",
|
||||
"nav_button_all_notifications": "Alle notificaties",
|
||||
"nav_button_documentation": "Documentatie",
|
||||
"nav_button_publish_message": "Notificatie publiceren",
|
||||
"nav_button_subscribe": "Onderwerp abonneren",
|
||||
"nav_button_subscribe": "Abonneer op onderwerp",
|
||||
"nav_button_muted": "Notificaties gedempt",
|
||||
"nav_button_connecting": "verbinden",
|
||||
"alert_grant_title": "Notificaties zijn uitgeschakeld",
|
||||
"alert_grant_description": "Geef je browser toestemming om meldingen weer te geven.",
|
||||
"alert_grant_description": "Verleen je browser toestemming voor het weergeven van notificaties.",
|
||||
"alert_grant_button": "Nu toestaan",
|
||||
"alert_not_supported_title": "Notificaties zijn niet ondersteund",
|
||||
"notifications_list": "Notificaties lijst",
|
||||
"notifications_list": "Notificatielijst",
|
||||
"notifications_list_item": "Notificatie",
|
||||
"notifications_mark_read": "Markeer als gelezen",
|
||||
"notifications_delete": "Verwijder",
|
||||
@@ -59,7 +59,7 @@
|
||||
"notifications_attachment_file_audio": "audiobestand",
|
||||
"notifications_attachment_file_app": "Android app bestand",
|
||||
"notifications_attachment_file_document": "overig document",
|
||||
"notifications_click_copy_url_title": "URL naar klembord kopiëren",
|
||||
"notifications_click_copy_url_title": "link URL naar klembord kopiëren",
|
||||
"notifications_click_copy_url_button": "Link kopiëren",
|
||||
"notifications_click_open_button": "Link openen",
|
||||
"notifications_none_for_topic_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar de URL van het onderwerp.",
|
||||
@@ -73,7 +73,7 @@
|
||||
"publish_dialog_title_no_topic": "Notificatie publiceren",
|
||||
"publish_dialog_progress_uploading": "Uploaden …",
|
||||
"notifications_actions_open_url_title": "Ga naar {{url}}",
|
||||
"notifications_actions_not_supported": "Deze actie is niet ondersteund in de web applicatie",
|
||||
"notifications_actions_not_supported": "Actie wordt niet ondersteund in de webapplicatie",
|
||||
"notifications_actions_http_request_title": "Stuur HTTP {{method}} naar {{url}}",
|
||||
"notifications_none_for_topic_title": "Je hebt nog geen notificaties ontvangen voor dit onderwerp.",
|
||||
"publish_dialog_priority_low": "Lage prioriteit",
|
||||
|
||||
191
web/public/static/langs/pt.json
Normal file
191
web/public/static/langs/pt.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"action_bar_clear_notifications": "Limpar todas as notificações",
|
||||
"action_bar_send_test_notification": "Enviar notificação de teste",
|
||||
"action_bar_unsubscribe": "Anular subscrição",
|
||||
"action_bar_toggle_mute": "Ativa/Desativa notificações",
|
||||
"action_bar_toggle_action_menu": "Abrir/fechar menu de ação",
|
||||
"message_bar_type_message": "Escreva uma mensagem aqui",
|
||||
"message_bar_error_publishing": "Erro ao publicar notificação",
|
||||
"message_bar_publish": "Publicar mensagem",
|
||||
"nav_topics_title": "Tópicos subscritos",
|
||||
"nav_button_all_notifications": "Todas notificações",
|
||||
"nav_button_settings": "Configurações",
|
||||
"nav_button_documentation": "Documentação",
|
||||
"nav_button_publish_message": "Publicar notificação",
|
||||
"nav_button_subscribe": "Subscrever tópico",
|
||||
"nav_button_muted": "Notificações desativadas",
|
||||
"nav_button_connecting": "A ligar",
|
||||
"alert_grant_title": "As notificações estão desativadas",
|
||||
"alert_grant_description": "Conceder permissão ao seu navegador para mostrar notificações.",
|
||||
"alert_not_supported_title": "Notificações não suportadas",
|
||||
"notifications_list": "Lista de notificações",
|
||||
"alert_not_supported_description": "As notificações não são suportadas pelo seu navegador.",
|
||||
"notifications_list_item": "Notificação",
|
||||
"notifications_mark_read": "Marcar como lido",
|
||||
"notifications_delete": "Apagar",
|
||||
"notifications_copied_to_clipboard": "Copiado para a área de transferência",
|
||||
"notifications_tags": "Etiquetas",
|
||||
"notifications_priority_x": "Prioridade {{priority}}",
|
||||
"notifications_new_indicator": "Nova notificação",
|
||||
"notifications_attachment_image": "Imagem anexada",
|
||||
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
|
||||
"notifications_attachment_copy_url_button": "Copiar URL",
|
||||
"notifications_attachment_open_title": "Ir para {{url}}",
|
||||
"notifications_attachment_link_expired": "a ligação de transferência expirou",
|
||||
"notifications_attachment_open_button": "Abrir anexo",
|
||||
"notifications_attachment_link_expires": "a ligação expira em {{date}}",
|
||||
"notifications_attachment_file_image": "ficheiro de imagem",
|
||||
"notifications_attachment_file_video": "ficheiro de vídeo",
|
||||
"notifications_attachment_file_audio": "ficheiro de áudio",
|
||||
"notifications_attachment_file_app": "ficheiro apk Android",
|
||||
"notifications_attachment_file_document": "outros documentos",
|
||||
"notifications_click_copy_url_title": "Copiar URL da ligação para a área de transferência",
|
||||
"notifications_click_copy_url_button": "Copiar ligação",
|
||||
"notifications_click_open_button": "Abrir ligação",
|
||||
"notifications_actions_open_url_title": "Ir para {{url}}",
|
||||
"notifications_actions_not_supported": "Ação não suportada na app web",
|
||||
"notifications_actions_http_request_title": "Enviar HTTP {{method}} para {{url}}",
|
||||
"notifications_none_for_topic_title": "Ainda não recebeu nenhuma notificação deste tópico.",
|
||||
"notifications_none_for_topic_description": "Para enviar notificações deste tópico, basta usar os métodos PUT ou POST no URL do tópico.",
|
||||
"notifications_none_for_any_title": "Ainda não recebeu nenhuma notificação.",
|
||||
"notifications_none_for_any_description": "Para enviar notificações dum tópico, basta usar os métodos PUT ou POST no URL do tópico. Eis um exemplo usando um dos seus tópicos.",
|
||||
"notifications_no_subscriptions_title": "Parece que ainda não tem nenhuma inscrição.",
|
||||
"notifications_no_subscriptions_description": "Clique na ligação \"{{linktext}}\" para criar ou subscrever um tópico. Depois, poderá enviar mensagens via PUT ou POST e receberá notificações aqui.",
|
||||
"notifications_example": "Exemplo",
|
||||
"notifications_more_details": "Para mais informações, aceda ao <websiteLink>site</websiteLink> ou à <docsLink>documentação</docsLink>.",
|
||||
"notifications_loading": "A carregar notificações…",
|
||||
"publish_dialog_title_topic": "Publicar em {{topic}}",
|
||||
"publish_dialog_title_no_topic": "Publicar notificação",
|
||||
"publish_dialog_progress_uploading": "A enviar …",
|
||||
"publish_dialog_progress_uploading_detail": "A enviar {{loaded}}/{{total}} ({{percent}}%)…",
|
||||
"publish_dialog_message_published": "Notificação publicada",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "excede limite de ficheiro de {{fileSizeLimit}} e cota, {{remainingBytes}} restante(s)",
|
||||
"publish_dialog_attachment_limits_quota_reached": "excede a cota, {{remainingBytes}} restante(s)",
|
||||
"publish_dialog_priority_min": "Prioridade mínima",
|
||||
"publish_dialog_priority_low": "Prioridade baixa",
|
||||
"publish_dialog_priority_default": "Prioridade padrão",
|
||||
"publish_dialog_priority_high": "Prioridade alta",
|
||||
"publish_dialog_base_url_label": "URL de serviço",
|
||||
"publish_dialog_base_url_placeholder": "URL de serviço, por exemplo: https://exemplo.com",
|
||||
"publish_dialog_topic_label": "Nome do tópico",
|
||||
"publish_dialog_topic_placeholder": "Nome do tópico, por exemplo: \"avisos_do_filipe\"",
|
||||
"publish_dialog_topic_reset": "Limpar tópico",
|
||||
"publish_dialog_title_placeholder": "Título da notificação, por exemplo: \"Alerta de espaço em disco\"",
|
||||
"publish_dialog_message_label": "Mensagem",
|
||||
"publish_dialog_message_placeholder": "Escreva uma mensagem aqui",
|
||||
"publish_dialog_tags_label": "Etiquetas",
|
||||
"publish_dialog_tags_placeholder": "Lista de etiquetas, separadas por vírgula, por exemplo: aviso, srv1-backup",
|
||||
"publish_dialog_priority_label": "Prioridade",
|
||||
"publish_dialog_click_label": "URL de clique",
|
||||
"publish_dialog_click_placeholder": "URL que é aberto quando a notificação é clicada",
|
||||
"publish_dialog_click_reset": "Remover URL de clique",
|
||||
"publish_dialog_email_label": "Email",
|
||||
"publish_dialog_filename_placeholder": "Nome do ficheiro anexado",
|
||||
"publish_dialog_email_placeholder": "Endereça para o qual encaminhar a notificação, por exemplo: filipe@exemplo.com",
|
||||
"publish_dialog_email_reset": "Remover encaminhamento por email",
|
||||
"publish_dialog_attach_label": "URL de anexo",
|
||||
"publish_dialog_attach_placeholder": "Anexar ficheiro por URL, por exemplo: https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_attach_reset": "Remover URL de anexo",
|
||||
"publish_dialog_filename_label": "Nome do ficheiro",
|
||||
"publish_dialog_delay_label": "Atraso",
|
||||
"publish_dialog_delay_placeholder": "Atraso na entrega, por exemplo \"{{{unixTimestamp}}\", \"{{relativeTime}}\", ou \"{{naturalLanguage}}\" (apenas em Inglês)",
|
||||
"publish_dialog_other_features": "Outras funcionalidades:",
|
||||
"publish_dialog_chip_click_label": "URL de clique",
|
||||
"publish_dialog_chip_topic_label": "Alterar tópico",
|
||||
"publish_dialog_details_examples_description": "Para obter exemplos e uma descrição detalhada de todas as funcionalidades de envio, consulte a <docsLink>documentação</docsLink>.",
|
||||
"publish_dialog_button_cancel_sending": "Cancelar o envio",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Nome do ficheiro anexado",
|
||||
"publish_dialog_attached_file_remove": "Remover ficheiro anexado",
|
||||
"emoji_picker_search_clear": "Limpar pesquisa",
|
||||
"subscribe_dialog_subscribe_description": "Os tópicos podem não ser protegidos por palavra-passe, por isso escolha um nome que não seja fácil de adivinhar. Uma vez subscrito, pode usar os métodos PUT/POST para publicar notificações.",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Usar outro servidor",
|
||||
"subscribe_dialog_error_user_not_authorized": "Utilizador {{username}} não autorizado",
|
||||
"prefs_notifications_min_priority_description_max": "Mostrar notificações se prioridade for 5 (máxima)",
|
||||
"prefs_notifications_delete_after_one_week": "Após uma semana",
|
||||
"prefs_notifications_delete_after_one_month": "Após um mês",
|
||||
"prefs_notifications_delete_after_never_description": "As notificações nunca serão eliminadas automaticamente",
|
||||
"prefs_notifications_delete_after_one_week_description": "As notificações serão eliminadas automaticamente após uma semana",
|
||||
"prefs_notifications_delete_after_one_month_description": "As notificações serão eliminadas automaticamente após um mês",
|
||||
"prefs_users_dialog_username_label": "Utilizador, por exemplo: \"filipe\"",
|
||||
"prefs_users_dialog_password_label": "Palavra-passe",
|
||||
"prefs_users_dialog_button_cancel": "Cancelar",
|
||||
"prefs_users_dialog_button_add": "Adicionar",
|
||||
"error_boundary_description": "Obviamente, isto não devia acontecer, lamentamos o sucedido.<br/>Se tiver um minuto, por favor <githubLink>relate isto no GitHub</githubLink>, ou informe-nos através de <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||
"error_boundary_stack_trace": "Erro (\"stack trace\")",
|
||||
"error_boundary_gathering_info": "A recolher mais informações …",
|
||||
"error_boundary_unsupported_indexeddb_title": "Navegação anónima não suportada",
|
||||
"error_boundary_unsupported_indexeddb_description": "A aplicação web ntfy necessita da \"IndexedDB\" para funcionar e o seu navegador não a suporta no modo de navegação privada.<br/><br/>Embora isso seja inconveniente, também não faz muito sentido usar a aplicação no modo de navegação privada de qualquer maneira, visto que tudo é guardado no armazenamento do navegador. Pode ler mais sobre isso <githubLink>nesta questão no GitHub</githubLink>, ou falar connosco por <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||
"action_bar_show_menu": "Mostrar menu",
|
||||
"action_bar_logo_alt": "logótipo do ntfy",
|
||||
"action_bar_settings": "Configurações",
|
||||
"message_bar_show_dialog": "Mostrar caixa de publicação",
|
||||
"alert_grant_button": "Conceder agora",
|
||||
"publish_dialog_attachment_limits_file_reached": "excede o limite de ficheiro de {{fileSizeLimit}}",
|
||||
"publish_dialog_emoji_picker_show": "Escolher emoji",
|
||||
"publish_dialog_priority_max": "Prioridade máxima",
|
||||
"publish_dialog_title_label": "Título",
|
||||
"publish_dialog_delay_reset": "Remover atraso de entrega",
|
||||
"publish_dialog_chip_email_label": "Encaminhar para email",
|
||||
"publish_dialog_chip_attach_url_label": "Anexar ficheiro por URL",
|
||||
"publish_dialog_chip_attach_file_label": "Anexar ficheiro local",
|
||||
"publish_dialog_chip_delay_label": "Atraso de entrega",
|
||||
"publish_dialog_button_cancel": "Cancelar",
|
||||
"publish_dialog_button_send": "Enviar",
|
||||
"publish_dialog_checkbox_publish_another": "Publicar outra",
|
||||
"publish_dialog_attached_file_title": "Ficheiro anexado:",
|
||||
"publish_dialog_drop_file_here": "Arraste o ficheiro para aqui",
|
||||
"emoji_picker_search_placeholder": "Pesquisar emoji",
|
||||
"subscribe_dialog_subscribe_title": "Subscrever tópico",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Nome do tópico, por exemplo: \"alertas_do_filipe\"",
|
||||
"subscribe_dialog_subscribe_base_url_label": "URL de serviço",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Cancelar",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "Subscrever",
|
||||
"subscribe_dialog_login_title": "Autenticação necessária",
|
||||
"subscribe_dialog_login_description": "Esse tópico é protegido por palavra-passe. Por favor insira um nome de utilizador e palavra-passe para subscrever.",
|
||||
"subscribe_dialog_login_username_label": "Nome, por exemplo: \"filipe\"",
|
||||
"subscribe_dialog_login_password_label": "Palavra-passe",
|
||||
"subscribe_dialog_login_button_back": "Voltar",
|
||||
"subscribe_dialog_login_button_login": "Autenticar",
|
||||
"subscribe_dialog_error_user_anonymous": "anónimo",
|
||||
"prefs_notifications_title": "Notificações",
|
||||
"prefs_notifications_sound_title": "Som de notificações",
|
||||
"prefs_notifications_sound_description_none": "Notificações não reproduzem nenhum som quando chegam",
|
||||
"prefs_notifications_sound_description_some": "Notificações reproduzem som {{sound}} quando chegam",
|
||||
"prefs_notifications_sound_no_sound": "Sem som",
|
||||
"prefs_notifications_sound_play": "Reproduzir som selecionado",
|
||||
"prefs_notifications_min_priority_title": "Prioridade mínima",
|
||||
"prefs_notifications_min_priority_description_any": "A mostrar todas as notificações, independentemente da prioridade",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "Mostrar notificações se prioridade for {{number}} ({{name}}) ou acima",
|
||||
"prefs_notifications_min_priority_any": "Qualquer prioridade",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Prioridade baixa e acima",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Prioridade alta e acima",
|
||||
"prefs_notifications_min_priority_max_only": "Apenas prioridade máxima",
|
||||
"prefs_notifications_delete_after_title": "Eliminar notificações",
|
||||
"prefs_notifications_delete_after_never": "Nunca",
|
||||
"prefs_notifications_delete_after_three_hours": "Após três horas",
|
||||
"prefs_notifications_delete_after_one_day": "Após um dia",
|
||||
"prefs_notifications_delete_after_three_hours_description": "As notificações serão eliminadas automaticamente após três horas",
|
||||
"prefs_notifications_delete_after_one_day_description": "As notificações serão eliminadas automaticamente após um dia",
|
||||
"prefs_users_title": "Gerir utilizadores",
|
||||
"prefs_users_description": "Adicionar/remover utilizadores aos seus tópicos protegidos. Note que o utilizador e palavra-passe são guardados no armazenamento local do navegador.",
|
||||
"prefs_users_table": "Tabela de utilizadores",
|
||||
"prefs_users_add_button": "Adicionar utilizador",
|
||||
"prefs_users_edit_button": "Editar utilizador",
|
||||
"prefs_users_delete_button": "Apagar utilizador",
|
||||
"prefs_users_table_user_header": "Utilizador",
|
||||
"prefs_users_table_base_url_header": "URL de serviço",
|
||||
"prefs_users_dialog_title_add": "Adicionar utilizador",
|
||||
"prefs_users_dialog_title_edit": "Editar utilizador",
|
||||
"prefs_users_dialog_base_url_label": "URL de serviço, por exemplo: https://ntfy.sh",
|
||||
"prefs_users_dialog_button_save": "Gravar",
|
||||
"prefs_appearance_title": "Aparência",
|
||||
"prefs_appearance_language_title": "Idioma",
|
||||
"priority_min": "mínima",
|
||||
"priority_low": "baixa",
|
||||
"priority_default": "padrão",
|
||||
"priority_high": "alta",
|
||||
"priority_max": "máxima",
|
||||
"error_boundary_title": "Oh não, o ntfy parou de funcionar",
|
||||
"error_boundary_button_copy_stack_trace": "Copiar erro (\"stack trace\")"
|
||||
}
|
||||
11
web/public/static/langs/ro.json
Normal file
11
web/public/static/langs/ro.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"action_bar_show_menu": "Afișează meniu",
|
||||
"action_bar_send_test_notification": "Trimite notificare de probă",
|
||||
"action_bar_clear_notifications": "Șterge toate notificările",
|
||||
"action_bar_settings": "Setări",
|
||||
"action_bar_unsubscribe": "Dezabonare",
|
||||
"action_bar_logo_alt": "logo-ul ntfy",
|
||||
"action_bar_toggle_mute": "Oprire/activare notificări",
|
||||
"message_bar_type_message": "Scrie un mesaj aici",
|
||||
"message_bar_error_publishing": "Eroare la publicarea notificării"
|
||||
}
|
||||
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 …"
|
||||
}
|
||||
@@ -10,15 +10,15 @@
|
||||
"notifications_list_item": "通知",
|
||||
"notifications_mark_read": "標示已讀",
|
||||
"notifications_attachment_image": "附加圖片",
|
||||
"notifications_attachment_copy_url_title": "複製附件URL到剪貼板",
|
||||
"notifications_attachment_copy_url_button": "複製URL",
|
||||
"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_title": "複製連結 URL 到剪貼板",
|
||||
"notifications_click_copy_url_button": "複製連結",
|
||||
"notifications_click_open_button": "開啟連結",
|
||||
"notifications_actions_not_supported": "網頁程式無法支援該動作",
|
||||
@@ -27,16 +27,16 @@
|
||||
"notifications_none_for_topic_description": "如要寄送通知到此主題,請使用 PUT 或 POST 到此主題URL。",
|
||||
"notifications_none_for_any_title": "尚未收到任何通知。",
|
||||
"action_bar_settings": "設定",
|
||||
"action_bar_send_test_notification": "寄送測試通知",
|
||||
"action_bar_send_test_notification": "發送測試通知",
|
||||
"action_bar_clear_notifications": "清除所有通知",
|
||||
"action_bar_show_menu": "顯示選單",
|
||||
"nav_button_documentation": "文件",
|
||||
"nav_button_publish_message": "發布通知",
|
||||
"nav_button_publish_message": "發佈通知",
|
||||
"nav_button_muted": "通知已靜音",
|
||||
"notifications_copied_to_clipboard": "複製到剪貼板",
|
||||
"message_bar_publish": "發布訊息",
|
||||
"message_bar_show_dialog": "顯示發布對話筐",
|
||||
"message_bar_error_publishing": "無法發布通知",
|
||||
"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": "設定",
|
||||
@@ -50,7 +50,36 @@
|
||||
"notifications_new_indicator": "新通知",
|
||||
"notifications_attachment_file_audio": "聲音檔案",
|
||||
"notifications_delete": "刪除",
|
||||
"notifications_attachment_link_expires": "連結已過期 {{date}}",
|
||||
"notifications_attachment_link_expires": "連結在 {{date}} 過期",
|
||||
"notifications_attachment_file_image": "圖片檔案",
|
||||
"notifications_actions_open_url_title": "前往 {{url}}"
|
||||
"notifications_actions_open_url_title": "前往 {{url}}",
|
||||
"notifications_no_subscriptions_title": "你尚未有任何訂閱。",
|
||||
"notifications_example": "範例",
|
||||
"notifications_more_details": "你可以在 <websiteLink>ntfy 網站</websiteLink>或者<docsLink>技術文件</docsLink>中查看更多資訊。",
|
||||
"notifications_loading": "載入中…",
|
||||
"publish_dialog_title_topic": "發佈到 {{topic}}",
|
||||
"publish_dialog_title_no_topic": "發佈通知",
|
||||
"publish_dialog_progress_uploading": "上傳中…",
|
||||
"publish_dialog_priority_label": "優先度",
|
||||
"publish_dialog_email_label": "電郵地址",
|
||||
"publish_dialog_filename_label": "檔案名稱",
|
||||
"publish_dialog_button_cancel": "取消",
|
||||
"publish_dialog_button_send": "傳送",
|
||||
"publish_dialog_button_cancel_sending": "取消傳送",
|
||||
"subscribe_dialog_subscribe_button_cancel": "取消",
|
||||
"subscribe_dialog_subscribe_button_subscribe": "訂閱",
|
||||
"emoji_picker_search_clear": "清除",
|
||||
"subscribe_dialog_login_password_label": "密碼",
|
||||
"subscribe_dialog_login_button_back": "返回",
|
||||
"subscribe_dialog_login_button_login": "登入",
|
||||
"prefs_notifications_delete_after_never": "從不",
|
||||
"prefs_users_add_button": "新增使用者",
|
||||
"prefs_users_dialog_password_label": "密碼",
|
||||
"prefs_users_dialog_title_add": "新增使用者",
|
||||
"prefs_users_dialog_button_save": "儲存",
|
||||
"prefs_users_dialog_button_cancel": "取消",
|
||||
"error_boundary_title": "歐買尬,ntfy 壞掉了",
|
||||
"notifications_none_for_any_description": "要開始發送通知到一個主題,只需要對主題 URL 發送 HTTP PUT 或者 POST,例如:",
|
||||
"notifications_no_subscriptions_description": "點選 「{{linktext}}」 連結以建立或訂閱主題。完成後,你就可以使用 HTTP PUT 或者 POST 發送通知到這裡了!",
|
||||
"error_boundary_description": "很抱歉 ntfy 發生錯誤了。<br/>如果你有時間,煩請到<githubLink> Github </githubLink>回報錯誤,或者到<discordLink> Discord </discordLink>或者<matrixLink> Matrix 聊天室</matrixLink>裡面告訴我們。"
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ export const unmatchedTags = (tags) => {
|
||||
else return tags.filter(tag => !(tag in emojis));
|
||||
}
|
||||
|
||||
|
||||
export const maybeWithBasicAuth = (headers, user) => {
|
||||
if (user) {
|
||||
headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
|
||||
@@ -241,3 +240,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
||||
yield chunk.substr(startIndex); // last line didn't end in a newline char
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -463,6 +463,7 @@ const Language = () => {
|
||||
<MenuItem value="nl">Nederlands</MenuItem>
|
||||
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
||||
<MenuItem value="uk">Українська</MenuItem>
|
||||
<MenuItem value="pt">Português</MenuItem>
|
||||
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
|
||||
<MenuItem value="pl">Polski</MenuItem>
|
||||
<MenuItem value="ru">Русский</MenuItem>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user