Compare commits
1 commit
main
...
http-respo
Author | SHA1 | Date | |
---|---|---|---|
|
db1a1fec0c |
|
@ -1,3 +0,0 @@
|
||||||
dist
|
|
||||||
*/node_modules
|
|
||||||
Dockerfile*
|
|
|
@ -1,11 +0,0 @@
|
||||||
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
|
||||||
|
|
||||||
# Run prettier (https://github.com/binwiederhier/ntfy/pull/746)
|
|
||||||
6f6a2d1f693070bf72e89d86748080e4825c9164
|
|
||||||
c87549e71a10bc789eac8036078228f06e515a8e
|
|
||||||
ca5d736a7169eb6b4b0d849e061d5bf9565dcc53
|
|
||||||
2e27f58963feb9e4d1c573d4745d07770777fa7d
|
|
||||||
|
|
||||||
# Run eslint (https://github.com/binwiederhier/ntfy/pull/748)
|
|
||||||
f558b4dbe9bb5b9e0e87fada1215de2558353173
|
|
||||||
8319f1cf26113167fb29fe12edaff5db74caf35f
|
|
26
.github/ISSUE_TEMPLATE/1_bug_report.md
vendored
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
name: 🐛 Bug Report
|
|
||||||
about: Report any errors and problems
|
|
||||||
title: ''
|
|
||||||
labels: '🪲 bug'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
:lady_beetle: **Describe the bug**
|
|
||||||
<!-- A clear and concise description of the problem. -->
|
|
||||||
|
|
||||||
:computer: **Components impacted**
|
|
||||||
<!-- ntfy server, Android app, iOS app, web app -->
|
|
||||||
|
|
||||||
:bulb: **Screenshots and/or logs**
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots or share logs help explain your problem.
|
|
||||||
To get logs from the ...
|
|
||||||
- ntfy server: Enable "log-level: trace" in your server.yml file
|
|
||||||
- Android app: Go to "Settings" -> "Record logs", then eventually "Copy/upload logs"
|
|
||||||
- web app: Press "F12" and find the "Console" window
|
|
||||||
-->
|
|
||||||
|
|
||||||
:crystal_ball: **Additional context**
|
|
||||||
<!-- Add any other context about the problem here. -->
|
|
26
.github/ISSUE_TEMPLATE/2_enhancement_request.md
vendored
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
name: 💡 Feature/Enhancement Request
|
|
||||||
about: Got a great idea? Let us know!
|
|
||||||
title: ''
|
|
||||||
labels: 'enhancement'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
|
|
||||||
sooner, and there are more people there to help!
|
|
||||||
|
|
||||||
- Discord: https://discord.gg/cT7ECsZj9w
|
|
||||||
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
:bulb: **Idea**
|
|
||||||
<!-- Share your thoughts; try to be detailed if you can -->
|
|
||||||
|
|
||||||
:computer: **Target components**
|
|
||||||
<!-- Where should this feature/enhancement be added? -->
|
|
||||||
<!-- e.g. ntfy server, Android app, iOS app, web app -->
|
|
||||||
|
|
21
.github/ISSUE_TEMPLATE/3_tech_support.md
vendored
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
name: 🆘 I need help with ...
|
|
||||||
about: Installing ntfy, configuring the app, etc.
|
|
||||||
title: ''
|
|
||||||
labels: 'tech-support'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
STOP!
|
|
||||||
|
|
||||||
This is not the right place to ask for help. Consider asking on Discord/Matrix instead.
|
|
||||||
You'll usually get an answer sooner, and there are more people there to help!
|
|
||||||
|
|
||||||
- Discord: https://discord.gg/cT7ECsZj9w
|
|
||||||
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
|
|
||||||
|
|
||||||
-->
|
|
21
.github/ISSUE_TEMPLATE/4_question.md
vendored
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
name: ❓ Question
|
|
||||||
about: Ask a question about ntfy
|
|
||||||
title: ''
|
|
||||||
labels: 'question'
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
|
|
||||||
sooner, and there are more people there to help!
|
|
||||||
|
|
||||||
- Discord: https://discord.gg/cT7ECsZj9w
|
|
||||||
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
:question: **Question**
|
|
||||||
<!-- Go ahead and ask your question here :) -->
|
|
BIN
.github/images/logo.png
vendored
Before Width: | Height: | Size: 81 KiB |
27
.github/workflows/build.yaml
vendored
|
@ -4,21 +4,30 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
-
|
-
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.20.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '17'
|
||||||
cache: 'npm'
|
-
|
||||||
cache-dependency-path: './web/package-lock.json'
|
name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Cache Go and npm modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/go/bin
|
||||||
|
~/.npm
|
||||||
|
web/node_modules
|
||||||
|
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-ntfy-
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: make build-deps-ubuntu
|
run: make build-deps-ubuntu
|
||||||
|
|
2
.github/workflows/docs.yaml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
cd build/ntfy-docs.github.io
|
cd build/ntfy-docs.github.io
|
||||||
git config user.name "GitHub Actions Bot"
|
git config user.name "GitHub Actions Bot"
|
||||||
git config user.email "<actions@github.com>"
|
git config user.email "<>"
|
||||||
git add docs/
|
git add docs/
|
||||||
git commit -m "Updated docs"
|
git commit -m "Updated docs"
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
27
.github/workflows/release.yaml
vendored
|
@ -7,21 +7,30 @@ jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
-
|
-
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.20.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '17'
|
||||||
cache: 'npm'
|
-
|
||||||
cache-dependency-path: './web/package-lock.json'
|
name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Cache Go and npm modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/go/bin
|
||||||
|
~/.npm
|
||||||
|
web/node_modules
|
||||||
|
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-ntfy-
|
||||||
-
|
-
|
||||||
name: Docker login
|
name: Docker login
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
|
|
27
.github/workflows/test.yaml
vendored
|
@ -4,21 +4,30 @@ jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
|
||||||
name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
-
|
-
|
||||||
name: Install Go
|
name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '1.20.x'
|
go-version: '1.19.x'
|
||||||
-
|
-
|
||||||
name: Install node
|
name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '17'
|
||||||
cache: 'npm'
|
-
|
||||||
cache-dependency-path: './web/package-lock.json'
|
name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
-
|
||||||
|
name: Cache Go and npm modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go/pkg/mod
|
||||||
|
~/go/bin
|
||||||
|
~/.npm
|
||||||
|
web/node_modules
|
||||||
|
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
|
||||||
|
restore-keys: ${{ runner.os }}-ntfy-
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: make build-deps-ubuntu
|
run: make build-deps-ubuntu
|
||||||
|
|
3
.gitignore
vendored
|
@ -1,5 +1,4 @@
|
||||||
dist/
|
dist/
|
||||||
dev-dist/
|
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
@ -12,5 +11,3 @@ secrets/
|
||||||
*.iml
|
*.iml
|
||||||
node_modules/
|
node_modules/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
__pycache__
|
|
||||||
web/dev-dist/
|
|
|
@ -71,7 +71,7 @@ builds:
|
||||||
nfpms:
|
nfpms:
|
||||||
-
|
-
|
||||||
package_name: ntfy
|
package_name: ntfy
|
||||||
homepage: https://git.zio.sh/astra/ntfy/v2
|
homepage: https://heckel.io/ntfy
|
||||||
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
|
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
|
||||||
description: Simple pub-sub notification service
|
description: Simple pub-sub notification service
|
||||||
license: Apache 2.0
|
license: Apache 2.0
|
||||||
|
@ -97,7 +97,7 @@ nfpms:
|
||||||
- dst: /var/lib/ntfy
|
- dst: /var/lib/ntfy
|
||||||
type: dir
|
type: dir
|
||||||
- dst: /usr/share/ntfy/logo.png
|
- dst: /usr/share/ntfy/logo.png
|
||||||
src: web/public/static/images/ntfy.png
|
src: web/public/static/img/ntfy.png
|
||||||
scripts:
|
scripts:
|
||||||
preinstall: "scripts/preinst.sh"
|
preinstall: "scripts/preinst.sh"
|
||||||
postinstall: "scripts/postinst.sh"
|
postinstall: "scripts/postinst.sh"
|
||||||
|
@ -119,6 +119,8 @@ archives:
|
||||||
- server/ntfy.service
|
- server/ntfy.service
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
- client/ntfy-client.service
|
- client/ntfy-client.service
|
||||||
|
replacements:
|
||||||
|
amd64: x86_64
|
||||||
-
|
-
|
||||||
id: ntfy_windows
|
id: ntfy_windows
|
||||||
builds:
|
builds:
|
||||||
|
@ -129,6 +131,8 @@ archives:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
|
replacements:
|
||||||
|
amd64: x86_64
|
||||||
-
|
-
|
||||||
id: ntfy_darwin
|
id: ntfy_darwin
|
||||||
builds:
|
builds:
|
||||||
|
@ -138,6 +142,8 @@ archives:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
- README.md
|
- README.md
|
||||||
- client/client.yml
|
- client/client.yml
|
||||||
|
replacements:
|
||||||
|
darwin: macOS
|
||||||
universal_binaries:
|
universal_binaries:
|
||||||
-
|
-
|
||||||
id: ntfy_darwin_all
|
id: ntfy_darwin_all
|
||||||
|
@ -164,14 +170,14 @@ dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile-arm
|
dockerfile: Dockerfile
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--platform=linux/arm64/v8"
|
- "--platform=linux/arm64/v8"
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile-arm
|
dockerfile: Dockerfile
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 7
|
goarm: 7
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
|
@ -179,7 +185,7 @@ dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
||||||
use: buildx
|
use: buildx
|
||||||
dockerfile: Dockerfile-arm
|
dockerfile: Dockerfile
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: 6
|
goarm: 6
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
|
|
13
Dockerfile
|
@ -1,16 +1,9 @@
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
|
|
||||||
LABEL org.opencontainers.image.url="https://ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
|
||||||
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
|
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|
||||||
LABEL org.opencontainers.image.title="ntfy"
|
|
||||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
|
||||||
|
|
||||||
RUN apk add --no-cache tzdata
|
|
||||||
COPY ntfy /usr/bin
|
COPY ntfy /usr/bin
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=60s --timeout=10s CMD wget -q --tries=1 http://localhost/v1/health -O - | grep -Eo '"healthy"\s*:\s*true' || exit 1
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
ENTRYPOINT ["ntfy"]
|
ENTRYPOINT ["ntfy"]
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
FROM alpine
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
|
|
||||||
LABEL org.opencontainers.image.url="https://ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
|
||||||
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
|
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|
||||||
LABEL org.opencontainers.image.title="ntfy"
|
|
||||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
|
||||||
|
|
||||||
# Alpine does not support adding "tzdata" on ARM anymore, see
|
|
||||||
# https://github.com/binwiederhier/ntfy/issues/894
|
|
||||||
|
|
||||||
COPY ntfy /usr/bin
|
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
|
||||||
ENTRYPOINT ["ntfy"]
|
|
|
@ -1,57 +0,0 @@
|
||||||
FROM golang:1.20-bullseye as builder
|
|
||||||
|
|
||||||
ARG VERSION=dev
|
|
||||||
ARG COMMIT=unknown
|
|
||||||
ARG NODE_MAJOR=18
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
build-essential ca-certificates curl gnupg \
|
|
||||||
&& mkdir -p /etc/apt/keyrings \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
|
||||||
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get install -y \
|
|
||||||
python3-pip nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ADD Makefile .
|
|
||||||
|
|
||||||
# docs
|
|
||||||
ADD ./requirements.txt .
|
|
||||||
RUN make docs-deps
|
|
||||||
ADD ./mkdocs.yml .
|
|
||||||
ADD ./docs ./docs
|
|
||||||
RUN make docs-build
|
|
||||||
|
|
||||||
# web
|
|
||||||
ADD ./web/package.json ./web/package-lock.json ./web/
|
|
||||||
RUN make web-deps
|
|
||||||
ADD ./web ./web
|
|
||||||
RUN make web-build
|
|
||||||
|
|
||||||
# cli & server
|
|
||||||
ADD go.mod go.sum main.go ./
|
|
||||||
ADD ./client ./client
|
|
||||||
ADD ./cmd ./cmd
|
|
||||||
ADD ./log ./log
|
|
||||||
ADD ./server ./server
|
|
||||||
ADD ./user ./user
|
|
||||||
ADD ./util ./util
|
|
||||||
RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
|
|
||||||
|
|
||||||
FROM alpine
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
|
|
||||||
LABEL org.opencontainers.image.url="https://ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
|
|
||||||
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
|
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|
||||||
LABEL org.opencontainers.image.title="ntfy"
|
|
||||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
|
||||||
|
|
||||||
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
|
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
|
||||||
ENTRYPOINT ["ntfy"]
|
|
71
Makefile
|
@ -31,16 +31,10 @@ help:
|
||||||
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
||||||
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build dev Docker:"
|
|
||||||
@echo " make docker-dev - Build client & server for current architecture using Docker only"
|
|
||||||
@echo
|
|
||||||
@echo "Build web app:"
|
@echo "Build web app:"
|
||||||
@echo " make web - Build the web app"
|
@echo " make web - Build the web app"
|
||||||
@echo " make web-deps - Install web app dependencies (npm install the universe)"
|
@echo " make web-deps - Install web app dependencies (npm install the universe)"
|
||||||
@echo " make web-build - Actually build the web app"
|
@echo " make web-build - Actually build the web app"
|
||||||
@echo " make web-lint - Run eslint on the web app"
|
|
||||||
@echo " make web-fmt - Run prettier on the web app"
|
|
||||||
@echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
|
|
||||||
@echo
|
@echo
|
||||||
@echo "Build documentation:"
|
@echo "Build documentation:"
|
||||||
@echo " make docs - Build the documentation"
|
@echo " make docs - Build the documentation"
|
||||||
|
@ -86,32 +80,34 @@ build: web docs cli
|
||||||
update: web-deps-update cli-deps-update docs-deps-update
|
update: web-deps-update cli-deps-update docs-deps-update
|
||||||
docker pull alpine
|
docker pull alpine
|
||||||
|
|
||||||
docker-dev:
|
|
||||||
docker build \
|
|
||||||
--file ./Dockerfile-build \
|
|
||||||
--tag binwiederhier/ntfy:$(VERSION) \
|
|
||||||
--tag binwiederhier/ntfy:dev \
|
|
||||||
--build-arg VERSION=$(VERSION) \
|
|
||||||
--build-arg COMMIT=$(COMMIT) \
|
|
||||||
./
|
|
||||||
|
|
||||||
# Ubuntu-specific
|
# Ubuntu-specific
|
||||||
|
|
||||||
build-deps-ubuntu:
|
build-deps-ubuntu:
|
||||||
sudo apt-get update
|
sudo apt update
|
||||||
sudo apt-get install -y \
|
sudo apt install -y \
|
||||||
curl \
|
curl \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
jq
|
jq
|
||||||
which pip3 || sudo apt-get install -y python3-pip
|
which pip3 || sudo apt install -y python3-pip
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
docs: docs-deps docs-build
|
docs: docs-deps docs-build
|
||||||
|
|
||||||
docs-build: .PHONY
|
docs-build: .PHONY
|
||||||
mkdocs build
|
@if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
|
||||||
|
if which python3.8; then \
|
||||||
|
echo "python3.8 $(shell which mkdocs) build"; \
|
||||||
|
python3.8 $(shell which mkdocs) build; \
|
||||||
|
else \
|
||||||
|
echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
else \
|
||||||
|
echo "mkdocs build"; \
|
||||||
|
mkdocs build; \
|
||||||
|
fi
|
||||||
|
|
||||||
docs-deps: .PHONY
|
docs-deps: .PHONY
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
@ -131,7 +127,8 @@ web-build:
|
||||||
&& rm -rf ../server/site \
|
&& rm -rf ../server/site \
|
||||||
&& mv build ../server/site \
|
&& mv build ../server/site \
|
||||||
&& rm \
|
&& rm \
|
||||||
../server/site/config.js
|
../server/site/config.js \
|
||||||
|
../server/site/asset-manifest.json
|
||||||
|
|
||||||
web-deps:
|
web-deps:
|
||||||
cd web && npm install
|
cd web && npm install
|
||||||
|
@ -140,37 +137,29 @@ web-deps:
|
||||||
web-deps-update:
|
web-deps-update:
|
||||||
cd web && npm update
|
cd web && npm update
|
||||||
|
|
||||||
web-fmt:
|
|
||||||
cd web && npm run format
|
|
||||||
|
|
||||||
web-fmt-check:
|
|
||||||
cd web && npm run format:check
|
|
||||||
|
|
||||||
web-lint:
|
|
||||||
cd web && npm run lint
|
|
||||||
|
|
||||||
# Main server/client build
|
# Main server/client build
|
||||||
|
|
||||||
cli: cli-deps
|
cli: cli-deps
|
||||||
goreleaser build --snapshot --clean
|
goreleaser build --snapshot --rm-dist
|
||||||
|
|
||||||
cli-linux-amd64: cli-deps-static-sites
|
cli-linux-amd64: cli-deps-static-sites
|
||||||
goreleaser build --snapshot --clean --id ntfy_linux_amd64
|
goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64
|
||||||
|
|
||||||
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||||
goreleaser build --snapshot --clean --id ntfy_linux_armv6
|
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6
|
||||||
|
|
||||||
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||||
goreleaser build --snapshot --clean --id ntfy_linux_armv7
|
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7
|
||||||
|
|
||||||
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
|
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
|
||||||
goreleaser build --snapshot --clean --id ntfy_linux_arm64
|
goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64
|
||||||
|
|
||||||
cli-windows-amd64: cli-deps-static-sites
|
cli-windows-amd64: cli-deps-static-sites
|
||||||
goreleaser build --snapshot --clean --id ntfy_windows_amd64
|
goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64
|
||||||
|
|
||||||
cli-darwin-all: cli-deps-static-sites
|
cli-darwin-all: cli-deps-static-sites
|
||||||
goreleaser build --snapshot --clean --id ntfy_darwin_all
|
goreleaser build --snapshot --rm-dist --id ntfy_darwin_all
|
||||||
|
|
||||||
cli-linux-server: cli-deps-static-sites
|
cli-linux-server: cli-deps-static-sites
|
||||||
# This is a target to build the CLI (including the server) manually.
|
# This is a target to build the CLI (including the server) manually.
|
||||||
|
@ -237,7 +226,7 @@ cli-build-results:
|
||||||
|
|
||||||
# Test/check targets
|
# Test/check targets
|
||||||
|
|
||||||
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
|
check: test fmt-check vet lint staticcheck
|
||||||
|
|
||||||
test: .PHONY
|
test: .PHONY
|
||||||
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
||||||
|
@ -264,7 +253,7 @@ coverage-upload:
|
||||||
|
|
||||||
# Lint/formatting targets
|
# Lint/formatting targets
|
||||||
|
|
||||||
fmt: web-fmt
|
fmt:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
|
||||||
fmt-check:
|
fmt-check:
|
||||||
|
@ -288,11 +277,11 @@ staticcheck: .PHONY
|
||||||
|
|
||||||
# Releasing targets
|
# Releasing targets
|
||||||
|
|
||||||
release: clean cli-deps release-checks docs web check
|
release: clean update cli-deps release-checks docs web check
|
||||||
goreleaser release --clean
|
goreleaser release --rm-dist
|
||||||
|
|
||||||
release-snapshot: clean cli-deps docs web check
|
release-snapshot: clean update cli-deps docs web check
|
||||||
goreleaser release --snapshot --skip-publish --clean
|
goreleaser release --snapshot --skip-publish --rm-dist
|
||||||
|
|
||||||
release-checks:
|
release-checks:
|
||||||
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
|
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
|
||||||
|
|
157
README.md
|
@ -1,9 +1,156 @@
|
||||||
|

|
||||||
|
|
||||||
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
||||||
|
[](https://github.com/binwiederhier/ntfy/releases/latest)
|
||||||
|
[](https://pkg.go.dev/heckel.io/ntfy)
|
||||||
|
[](https://github.com/binwiederhier/ntfy/actions)
|
||||||
|
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
|
||||||
|
[](https://codecov.io/gh/binwiederhier/ntfy)
|
||||||
|
[](https://discord.gg/cT7ECsZj9w)
|
||||||
|
[](https://matrix.to/#/#ntfy:matrix.org)
|
||||||
|
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||||
|
[](https://www.reddit.com/r/ntfy/)
|
||||||
|
[](https://ntfy.statuspage.io/)
|
||||||
|
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||||
|
|
||||||
**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
|
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
|
||||||
notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer,
|
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
|
||||||
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
|
It's also open source (as you can plainly see) if you want to run your own.
|
||||||
so since ntfy is open source.
|
|
||||||
|
|
||||||
|
I run a free version of it at **[ntfy.sh](https://ntfy.sh)**. There's also an [open source Android app](https://github.com/binwiederhier/ntfy-android) (see [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/)), and an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) (see [App Store](https://apps.apple.com/us/app/ntfy/id1625396347)).
|
||||||
|
|
||||||
### This is a fork of [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy)
|
<p>
|
||||||
|
<img src="web/public/static/img/screenshot-curl.png" height="180">
|
||||||
|
<img src="web/public/static/img/screenshot-web-detail.png" height="180">
|
||||||
|
<img src="web/public/static/img/screenshot-phone-main.jpg" height="180">
|
||||||
|
<img src="web/public/static/img/screenshot-phone-detail.jpg" height="180">
|
||||||
|
<img src="web/public/static/img/screenshot-phone-notification.jpg" height="180">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## **[Documentation](https://ntfy.sh/docs/)**
|
||||||
|
|
||||||
|
[Getting started](https://ntfy.sh/docs/) |
|
||||||
|
[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
|
||||||
|
[API](https://ntfy.sh/docs/publish/) |
|
||||||
|
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
|
||||||
|
[Building](https://ntfy.sh/docs/develop/)
|
||||||
|
|
||||||
|
## Chat / forum
|
||||||
|
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
|
||||||
|
works best for you:
|
||||||
|
|
||||||
|
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
||||||
|
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
||||||
|
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
|
||||||
|
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||||
|
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
|
||||||
|
|
||||||
|
## Announcements / beta testers
|
||||||
|
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
|
||||||
|
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
|
||||||
|
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||||
|
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||||
|
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||||
|
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||||
|
|
||||||
|
<a href="https://hosted.weblate.org/engage/ntfy/">
|
||||||
|
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
||||||
|
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
||||||
|
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
||||||
|
|
||||||
|
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/nickexyz"><img src="https://github.com/nickexyz.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/qcasey"><img src="https://github.com/qcasey.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/mckay115"><img src="https://github.com/mckay115.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Salamafet"><img src="https://github.com/Salamafet.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/codinghipster"><img src="https://github.com/codinghipster.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/HinFort"><img src="https://github.com/HinFort.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Lexevolution"><img src="https://github.com/Lexevolution.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/johnnyip"><img src="https://github.com/johnnyip.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/JonDerThan"><img src="https://github.com/JonDerThan.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/12nick12"><img src="https://github.com/12nick12.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/eanplatter"><img src="https://github.com/eanplatter.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/fnoelscher"><img src="https://github.com/fnoelscher.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/bnorick"><img src="https://github.com/bnorick.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/snh"><img src="https://github.com/snh.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/hen-x"><img src="https://github.com/hen-x.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/JamieGoodson"><img src="https://github.com/JamieGoodson.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/cremesk"><img src="https://github.com/cremesk.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/dangowans"><img src="https://github.com/dangowans.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/tonyakwei"><img src="https://github.com/tonyakwei.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/crosbyh"><img src="https://github.com/crosbyh.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/mdlnr"><img src="https://github.com/mdlnr.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/p-samuel"><img src="https://github.com/p-samuel.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/zugaldia"><img src="https://github.com/zugaldia.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/NathanSweet"><img src="https://github.com/NathanSweet.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/msdeibel"><img src="https://github.com/msdeibel.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/ksurl"><img src="https://github.com/ksurl.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/CodingTimeDEV"><img src="https://github.com/CodingTimeDEV.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Terrormixer3000"><img src="https://github.com/Terrormixer3000.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/voroskoi"><img src="https://github.com/voroskoi.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Nickwasused"><img src="https://github.com/Nickwasused.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/vinhdizzo"><img src="https://github.com/vinhdizzo.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Ge0rg3"><img src="https://github.com/Ge0rg3.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/biopsin"><img src="https://github.com/biopsin.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/thebino"><img src="https://github.com/thebino.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/sky4055"><img src="https://github.com/sky4055.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/julianlam"><img src="https://github.com/julianlam.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/andreapx"><img src="https://github.com/andreapx.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/billycao"><img src="https://github.com/billycao.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/zoic21"><img src="https://github.com/zoic21.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/IanKulin"><img src="https://github.com/IanKulin.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Joachim256"><img src="https://github.com/Joachim256.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/overtone1000"><img src="https://github.com/overtone1000.png" width="40px" /></a>
|
||||||
|
|
||||||
|
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
|
||||||
|
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||||
|
|
||||||
|
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
|
||||||
|
|
||||||
|
_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
|
||||||
|
|
||||||
|
## License
|
||||||
|
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
|
||||||
|
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
|
||||||
|
|
||||||
|
Third party libraries and resources:
|
||||||
|
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
|
||||||
|
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
|
||||||
|
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
|
||||||
|
* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web
|
||||||
|
* [React](https://reactjs.org/) (MIT) is used for the web app
|
||||||
|
* [Material UI components](https://mui.com/) (MIT) are used in the web app
|
||||||
|
* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app
|
||||||
|
* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB
|
||||||
|
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
|
||||||
|
* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails
|
||||||
|
* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests
|
||||||
|
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
|
||||||
|
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
|
||||||
|
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
|
||||||
|
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page
|
||||||
|
* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files
|
||||||
|
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
|
||||||
|
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
|
||||||
|
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
|
||||||
|
|
10
SECURITY.md
|
@ -1,10 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
As of today, I only support the latest version of ntfy. Please make sure you stay up-to-date.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please report severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w),
|
|
||||||
or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`).
|
|
|
@ -7,29 +7,27 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
"heckel.io/ntfy/log"
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Event type constants
|
||||||
const (
|
const (
|
||||||
// MessageEvent identifies a message event
|
MessageEvent = "message"
|
||||||
MessageEvent = "message"
|
KeepaliveEvent = "keepalive"
|
||||||
|
OpenEvent = "open"
|
||||||
|
PollRequestEvent = "poll_request"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxResponseBytes = 4096
|
maxResponseBytes = 4096
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
|
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Messages chan *Message
|
Messages chan *Message
|
||||||
|
@ -98,14 +96,8 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess
|
||||||
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
|
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
|
||||||
// WithNoFirebase, and the generic WithHeader.
|
// WithNoFirebase, and the generic WithHeader.
|
||||||
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
|
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
|
||||||
topicURL, err := c.expandTopicURL(topic)
|
topicURL := c.expandTopicURL(topic)
|
||||||
if err != nil {
|
req, _ := http.NewRequest("POST", topicURL, body)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", topicURL, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if err := option(req); err != nil {
|
if err := option(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -141,14 +133,11 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
|
||||||
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
|
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
|
||||||
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
|
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
|
||||||
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
|
||||||
topicURL, err := c.expandTopicURL(topic)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
messages := make([]*Message, 0)
|
messages := make([]*Message, 0)
|
||||||
msgChan := make(chan *Message)
|
msgChan := make(chan *Message)
|
||||||
errChan := make(chan error)
|
errChan := make(chan error)
|
||||||
|
topicURL := c.expandTopicURL(topic)
|
||||||
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
|
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
|
||||||
options = append(options, WithPoll())
|
options = append(options, WithPoll())
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -177,18 +166,15 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// c := client.New(client.NewConfig())
|
// c := client.New(client.NewConfig())
|
||||||
// subscriptionID, _ := c.Subscribe("mytopic")
|
// subscriptionID := c.Subscribe("mytopic")
|
||||||
// for m := range c.Messages {
|
// for m := range c.Messages {
|
||||||
// fmt.Printf("New message: %s", m.Message)
|
// fmt.Printf("New message: %s", m.Message)
|
||||||
// }
|
// }
|
||||||
func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
|
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
|
||||||
topicURL, err := c.expandTopicURL(topic)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
subscriptionID := util.RandomString(10)
|
subscriptionID := util.RandomString(10)
|
||||||
|
topicURL := c.expandTopicURL(topic)
|
||||||
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
|
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
c.subscriptions[subscriptionID] = &subscription{
|
c.subscriptions[subscriptionID] = &subscription{
|
||||||
|
@ -197,7 +183,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, er
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
|
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
|
||||||
return subscriptionID, nil
|
return subscriptionID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
|
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
|
||||||
|
@ -213,16 +199,31 @@ func (c *Client) Unsubscribe(subscriptionID string) {
|
||||||
sub.cancel()
|
sub.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) expandTopicURL(topic string) (string, error) {
|
// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe.
|
||||||
|
// If there are multiple subscriptions matching the topic, all of them are unsubscribed from.
|
||||||
|
//
|
||||||
|
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
|
||||||
|
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
|
||||||
|
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
|
||||||
|
func (c *Client) UnsubscribeAll(topic string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
topicURL := c.expandTopicURL(topic)
|
||||||
|
for _, sub := range c.subscriptions {
|
||||||
|
if sub.topicURL == topicURL {
|
||||||
|
delete(c.subscriptions, sub.ID)
|
||||||
|
sub.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) expandTopicURL(topic string) string {
|
||||||
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
|
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
|
||||||
return topic, nil
|
return topic
|
||||||
} else if strings.Contains(topic, "/") {
|
} else if strings.Contains(topic, "/") {
|
||||||
return fmt.Sprintf("https://%s", topic), nil
|
return fmt.Sprintf("https://%s", topic)
|
||||||
}
|
}
|
||||||
if !topicRegex.MatchString(topic) {
|
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
|
||||||
return "", fmt.Errorf("invalid topic name: %s", topic)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
|
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
|
||||||
|
|
|
@ -5,15 +5,10 @@
|
||||||
#
|
#
|
||||||
# default-host: https://ntfy.sh
|
# default-host: https://ntfy.sh
|
||||||
|
|
||||||
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
|
||||||
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
|
||||||
# use empty double-quotes ("").
|
# For an empty password, use empty double-quotes ("")
|
||||||
#
|
#
|
||||||
# To override the default user:password combination or default token for a particular subscription (e.g., to send
|
|
||||||
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").
|
|
||||||
|
|
||||||
# default-token:
|
|
||||||
|
|
||||||
# default-user:
|
# default-user:
|
||||||
# default-password:
|
# default-password:
|
||||||
|
|
||||||
|
@ -35,8 +30,6 @@
|
||||||
# command: 'notify-send "$m"'
|
# command: 'notify-send "$m"'
|
||||||
# user: phill
|
# user: phill
|
||||||
# password: mypass
|
# password: mypass
|
||||||
# - topic: token_topic
|
|
||||||
# token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
#
|
#
|
||||||
# Variables:
|
# Variables:
|
||||||
# Variable Aliases Description
|
# Variable Aliases Description
|
||||||
|
|
|
@ -2,10 +2,10 @@ package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) {
|
||||||
defer test.StopServer(t, s, port)
|
defer test.StopServer(t, s, port)
|
||||||
c := client.New(newTestConfig(port))
|
c := client.New(newTestConfig(port))
|
||||||
|
|
||||||
subscriptionID, _ := c.Subscribe("mytopic")
|
subscriptionID := c.Subscribe("mytopic")
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
msg, err := c.Publish("mytopic", "some message")
|
msg, err := c.Publish("mytopic", "some message")
|
||||||
|
|
|
@ -12,22 +12,17 @@ const (
|
||||||
|
|
||||||
// Config is the config struct for a Client
|
// Config is the config struct for a Client
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultHost string `yaml:"default-host"`
|
DefaultHost string `yaml:"default-host"`
|
||||||
DefaultUser string `yaml:"default-user"`
|
DefaultUser string `yaml:"default-user"`
|
||||||
DefaultPassword *string `yaml:"default-password"`
|
DefaultPassword *string `yaml:"default-password"`
|
||||||
DefaultToken string `yaml:"default-token"`
|
DefaultCommand string `yaml:"default-command"`
|
||||||
DefaultCommand string `yaml:"default-command"`
|
Subscribe []struct {
|
||||||
Subscribe []Subscribe `yaml:"subscribe"`
|
Topic string `yaml:"topic"`
|
||||||
}
|
User string `yaml:"user"`
|
||||||
|
Password *string `yaml:"password"`
|
||||||
// Subscribe is the struct for a Subscription within Config
|
Command string `yaml:"command"`
|
||||||
type Subscribe struct {
|
If map[string]string `yaml:"if"`
|
||||||
Topic string `yaml:"topic"`
|
} `yaml:"subscribe"`
|
||||||
User *string `yaml:"user"`
|
|
||||||
Password *string `yaml:"password"`
|
|
||||||
Token *string `yaml:"token"`
|
|
||||||
Command string `yaml:"command"`
|
|
||||||
If map[string]string `yaml:"if"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new Config struct for a Client
|
// NewConfig creates a new Config struct for a Client
|
||||||
|
@ -36,7 +31,6 @@ func NewConfig() *Config {
|
||||||
DefaultHost: DefaultBaseURL,
|
DefaultHost: DefaultBaseURL,
|
||||||
DefaultUser: "",
|
DefaultUser: "",
|
||||||
DefaultPassword: nil,
|
DefaultPassword: nil,
|
||||||
DefaultToken: "",
|
|
||||||
DefaultCommand: "",
|
DefaultCommand: "",
|
||||||
Subscribe: nil,
|
Subscribe: nil,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package client_test
|
package client_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -37,7 +37,7 @@ subscribe:
|
||||||
require.Equal(t, 4, len(conf.Subscribe))
|
require.Equal(t, 4, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||||
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
|
||||||
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
|
||||||
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
|
||||||
|
@ -67,7 +67,7 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||||
require.Equal(t, "", *conf.Subscribe[0].Password)
|
require.Equal(t, "", *conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,28 +113,6 @@ subscribe:
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
|
||||||
require.Equal(t, "", conf.Subscribe[0].Command)
|
require.Equal(t, "", conf.Subscribe[0].Command)
|
||||||
require.Equal(t, "phil", *conf.Subscribe[0].User)
|
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_DefaultToken(t *testing.T) {
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
|
||||||
default-host: http://localhost
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
`), 0600))
|
|
||||||
|
|
||||||
conf, err := client.LoadConfig(filename)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
|
||||||
require.Equal(t, "", conf.DefaultUser)
|
|
||||||
require.Nil(t, conf.DefaultPassword)
|
|
||||||
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
|
||||||
require.Equal(t, 1, len(conf.Subscribe))
|
|
||||||
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
|
||||||
require.Nil(t, conf.Subscribe[0].User)
|
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
|
||||||
require.Nil(t, conf.Subscribe[0].Token)
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
"heckel.io/ntfy/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -72,11 +72,6 @@ func WithAttach(attach string) PublishOption {
|
||||||
return WithHeader("X-Attach", attach)
|
return WithHeader("X-Attach", attach)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithMarkdown instructs the server to interpret the message body as Markdown
|
|
||||||
func WithMarkdown() PublishOption {
|
|
||||||
return WithHeader("X-Markdown", "yes")
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
|
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
|
||||||
func WithFilename(filename string) PublishOption {
|
func WithFilename(filename string) PublishOption {
|
||||||
return WithHeader("X-Filename", filename)
|
return WithHeader("X-Filename", filename)
|
||||||
|
@ -97,11 +92,6 @@ func WithBearerAuth(token string) PublishOption {
|
||||||
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithEmptyAuth clears the Authorization header
|
|
||||||
func WithEmptyAuth() PublishOption {
|
|
||||||
return RemoveHeader("Authorization")
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNoCache instructs the server not to cache the message server-side
|
// WithNoCache instructs the server not to cache the message server-side
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
return WithHeader("X-Cache", "no")
|
return WithHeader("X-Cache", "no")
|
||||||
|
@ -192,13 +182,3 @@ func WithQueryParam(param, value string) RequestOption {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHeader is a generic option to remove a header from a request
|
|
||||||
func RemoveHeader(header string) RequestOption {
|
|
||||||
return func(r *http.Request) error {
|
|
||||||
if header != "" {
|
|
||||||
delete(r.Header, header)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,9 +5,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,9 +3,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -31,7 +31,6 @@ var flagsPublish = append(
|
||||||
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
|
||||||
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
|
||||||
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
|
||||||
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
|
|
||||||
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
|
||||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||||
|
@ -41,6 +40,7 @@ var flagsPublish = append(
|
||||||
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
|
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
|
||||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
||||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"no_firebase", "F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"no_firebase", "F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
||||||
|
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"env_topic", "P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
|
||||||
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"},
|
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -96,7 +96,6 @@ func execPublish(c *cli.Context) error {
|
||||||
icon := c.String("icon")
|
icon := c.String("icon")
|
||||||
actions := c.String("actions")
|
actions := c.String("actions")
|
||||||
attach := c.String("attach")
|
attach := c.String("attach")
|
||||||
markdown := c.Bool("markdown")
|
|
||||||
filename := c.String("filename")
|
filename := c.String("filename")
|
||||||
file := c.String("file")
|
file := c.String("file")
|
||||||
email := c.String("email")
|
email := c.String("email")
|
||||||
|
@ -142,9 +141,6 @@ func execPublish(c *cli.Context) error {
|
||||||
if attach != "" {
|
if attach != "" {
|
||||||
options = append(options, client.WithAttach(attach))
|
options = append(options, client.WithAttach(attach))
|
||||||
}
|
}
|
||||||
if markdown {
|
|
||||||
options = append(options, client.WithMarkdown())
|
|
||||||
}
|
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
options = append(options, client.WithFilename(filename))
|
options = append(options, client.WithFilename(filename))
|
||||||
}
|
}
|
||||||
|
@ -159,7 +155,8 @@ func execPublish(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
if token != "" {
|
if token != "" {
|
||||||
options = append(options, client.WithBearerAuth(token))
|
options = append(options, client.WithBearerAuth(token))
|
||||||
} else if user != "" {
|
}
|
||||||
|
if user != "" {
|
||||||
var pass string
|
var pass string
|
||||||
parts := strings.SplitN(user, ":", 2)
|
parts := strings.SplitN(user, ":", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
@ -175,8 +172,6 @@ func execPublish(c *cli.Context) error {
|
||||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||||
}
|
}
|
||||||
options = append(options, client.WithBasicAuth(user, pass))
|
options = append(options, client.WithBasicAuth(user, pass))
|
||||||
} else if conf.DefaultToken != "" {
|
|
||||||
options = append(options, client.WithBearerAuth(conf.DefaultToken))
|
|
||||||
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,11 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"net/http"
|
"heckel.io/ntfy/test"
|
||||||
"net/http/httptest"
|
"heckel.io/ntfy/util"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -133,11 +130,11 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||||
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
||||||
|
|
||||||
// Tests with NTFY_TOPIC set ////
|
// Tests with NTFY_TOPIC set ////
|
||||||
t.Setenv("NTFY_TOPIC", topic)
|
require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
|
||||||
|
|
||||||
// Test: Successful command with NTFY_TOPIC
|
// Test: Successful command with NTFY_TOPIC
|
||||||
app, _, stdout, _ = newTestApp()
|
app, _, stdout, _ = newTestApp()
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--cmd", "echo", "hi there"}))
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--cmd", "echo", "hi there"}))
|
||||||
m = toMessage(t, stdout.String())
|
m = toMessage(t, stdout.String())
|
||||||
require.Equal(t, "mytopic", m.Topic)
|
require.Equal(t, "mytopic", m.Topic)
|
||||||
|
|
||||||
|
@ -146,155 +143,7 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||||
require.Nil(t, sleep.Start())
|
require.Nil(t, sleep.Start())
|
||||||
go sleep.Wait() // Must be called to release resources
|
go sleep.Wait() // Must be called to release resources
|
||||||
app, _, stdout, _ = newTestApp()
|
app, _, stdout, _ = newTestApp()
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
|
||||||
m = toMessage(t, stdout.String())
|
m = toMessage(t, stdout.String())
|
||||||
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCLI_Publish_Default_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Default_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: fakepass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
|
||||||
m := toMessage(t, stdout.String())
|
|
||||||
require.Equal(t, "triggered", m.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
|
|
||||||
app, _, _, _ := newTestApp()
|
|
||||||
err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Equal(t, "cannot set both --user and --token", err.Error())
|
|
||||||
}
|
|
||||||
|
|
85
cmd/serve.go
|
@ -5,8 +5,8 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
|
||||||
"github.com/stripe/stripe-go/v74"
|
"github.com/stripe/stripe-go/v74"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
@ -17,12 +17,12 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
"heckel.io/ntfy/log"
|
||||||
|
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -37,8 +37,8 @@ var flagsServe = append(
|
||||||
append([]cli.Flag{}, flagsDefault...),
|
append([]cli.Flag{}, flagsDefault...),
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
|
||||||
|
@ -59,12 +59,11 @@ var flagsServe = append(
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
||||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
|
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
|
||||||
|
@ -72,10 +71,6 @@ var flagsServe = append(
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
|
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
|
||||||
|
@ -86,19 +81,9 @@ var flagsServe = append(
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var cmdServe = &cli.Command{
|
var cmdServe = &cli.Command{
|
||||||
|
@ -134,11 +119,6 @@ func execServe(c *cli.Context) error {
|
||||||
keyFile := c.String("key-file")
|
keyFile := c.String("key-file")
|
||||||
certFile := c.String("cert-file")
|
certFile := c.String("cert-file")
|
||||||
firebaseKeyFile := c.String("firebase-key-file")
|
firebaseKeyFile := c.String("firebase-key-file")
|
||||||
webPushPrivateKey := c.String("web-push-private-key")
|
|
||||||
webPushPublicKey := c.String("web-push-public-key")
|
|
||||||
webPushFile := c.String("web-push-file")
|
|
||||||
webPushEmailAddress := c.String("web-push-email-address")
|
|
||||||
webPushStartupQueries := c.String("web-push-startup-queries")
|
|
||||||
cacheFile := c.String("cache-file")
|
cacheFile := c.String("cache-file")
|
||||||
cacheDuration := c.Duration("cache-duration")
|
cacheDuration := c.Duration("cache-duration")
|
||||||
cacheStartupQueries := c.String("cache-startup-queries")
|
cacheStartupQueries := c.String("cache-startup-queries")
|
||||||
|
@ -159,7 +139,6 @@ func execServe(c *cli.Context) error {
|
||||||
enableLogin := c.Bool("enable-login")
|
enableLogin := c.Bool("enable-login")
|
||||||
enableReservations := c.Bool("enable-reservations")
|
enableReservations := c.Bool("enable-reservations")
|
||||||
upstreamBaseURL := c.String("upstream-base-url")
|
upstreamBaseURL := c.String("upstream-base-url")
|
||||||
upstreamAccessToken := c.String("upstream-access-token")
|
|
||||||
smtpSenderAddr := c.String("smtp-sender-addr")
|
smtpSenderAddr := c.String("smtp-sender-addr")
|
||||||
smtpSenderUser := c.String("smtp-sender-user")
|
smtpSenderUser := c.String("smtp-sender-user")
|
||||||
smtpSenderPass := c.String("smtp-sender-pass")
|
smtpSenderPass := c.String("smtp-sender-pass")
|
||||||
|
@ -167,13 +146,8 @@ func execServe(c *cli.Context) error {
|
||||||
smtpServerListen := c.String("smtp-server-listen")
|
smtpServerListen := c.String("smtp-server-listen")
|
||||||
smtpServerDomain := c.String("smtp-server-domain")
|
smtpServerDomain := c.String("smtp-server-domain")
|
||||||
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
|
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
|
||||||
twilioAccount := c.String("twilio-account")
|
|
||||||
twilioAuthToken := c.String("twilio-auth-token")
|
|
||||||
twilioPhoneNumber := c.String("twilio-phone-number")
|
|
||||||
twilioVerifyService := c.String("twilio-verify-service")
|
|
||||||
totalTopicLimit := c.Int("global-topic-limit")
|
totalTopicLimit := c.Int("global-topic-limit")
|
||||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
||||||
visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
|
|
||||||
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
||||||
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
|
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
|
||||||
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
||||||
|
@ -185,16 +159,10 @@ func execServe(c *cli.Context) error {
|
||||||
behindProxy := c.Bool("behind-proxy")
|
behindProxy := c.Bool("behind-proxy")
|
||||||
stripeSecretKey := c.String("stripe-secret-key")
|
stripeSecretKey := c.String("stripe-secret-key")
|
||||||
stripeWebhookKey := c.String("stripe-webhook-key")
|
stripeWebhookKey := c.String("stripe-webhook-key")
|
||||||
billingContact := c.String("billing-contact")
|
|
||||||
metricsListenHTTP := c.String("metrics-listen-http")
|
|
||||||
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
|
|
||||||
profileListenHTTP := c.String("profile-listen-http")
|
|
||||||
|
|
||||||
// Check values
|
// Check values
|
||||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||||
return errors.New("if set, FCM key file must exist")
|
return errors.New("if set, FCM key file must exist")
|
||||||
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
|
|
||||||
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
|
|
||||||
} else if keepaliveInterval < 5*time.Second {
|
} else if keepaliveInterval < 5*time.Second {
|
||||||
return errors.New("keepalive interval cannot be lower than five seconds")
|
return errors.New("keepalive interval cannot be lower than five seconds")
|
||||||
} else if managerInterval < 5*time.Second {
|
} else if managerInterval < 5*time.Second {
|
||||||
|
@ -207,8 +175,8 @@ func execServe(c *cli.Context) error {
|
||||||
return errors.New("if set, certificate file must exist")
|
return errors.New("if set, certificate file must exist")
|
||||||
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
|
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
|
||||||
return errors.New("if listen-https is set, both key-file and cert-file must be set")
|
return errors.New("if listen-https is set, both key-file and cert-file must be set")
|
||||||
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") {
|
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderUser == "" || smtpSenderPass == "" || smtpSenderFrom == "") {
|
||||||
return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set")
|
return errors.New("if smtp-sender-addr is set, base-url, smtp-sender-user, smtp-sender-pass and smtp-sender-from must also be set")
|
||||||
} else if smtpServerListen != "" && smtpServerDomain == "" {
|
} else if smtpServerListen != "" && smtpServerDomain == "" {
|
||||||
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
||||||
} else if attachmentCacheDir != "" && baseURL == "" {
|
} else if attachmentCacheDir != "" && baseURL == "" {
|
||||||
|
@ -217,6 +185,8 @@ func execServe(c *cli.Context) error {
|
||||||
return errors.New("if set, base-url must start with http:// or https://")
|
return errors.New("if set, base-url must start with http:// or https://")
|
||||||
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
||||||
return errors.New("if set, base-url must not end with a slash (/)")
|
return errors.New("if set, base-url must not end with a slash (/)")
|
||||||
|
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
|
||||||
|
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||||
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||||
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||||
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
||||||
|
@ -231,20 +201,10 @@ func execServe(c *cli.Context) error {
|
||||||
return errors.New("cannot set enable-signup without also setting enable-login")
|
return errors.New("cannot set enable-signup without also setting enable-login")
|
||||||
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
|
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
|
||||||
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
||||||
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
|
|
||||||
return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backwards compatibility
|
webRootIsApp := webRoot == "app"
|
||||||
if webRoot == "app" {
|
enableWeb := webRoot != "disable"
|
||||||
webRoot = "/"
|
|
||||||
} else if webRoot == "home" {
|
|
||||||
webRoot = "/app"
|
|
||||||
} else if webRoot == "disable" {
|
|
||||||
webRoot = ""
|
|
||||||
} else if !strings.HasPrefix(webRoot, "/") {
|
|
||||||
webRoot = "/" + webRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default auth permissions
|
// Default auth permissions
|
||||||
authDefault, err := user.ParsePermission(authDefaultAccess)
|
authDefault, err := user.ParsePermission(authDefaultAccess)
|
||||||
|
@ -290,7 +250,6 @@ func execServe(c *cli.Context) error {
|
||||||
|
|
||||||
// Stripe things
|
// Stripe things
|
||||||
if stripeSecretKey != "" {
|
if stripeSecretKey != "" {
|
||||||
stripe.EnableTelemetry = false // Whoa!
|
|
||||||
stripe.Key = stripeSecretKey
|
stripe.Key = stripeSecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,9 +282,8 @@ func execServe(c *cli.Context) error {
|
||||||
conf.KeepaliveInterval = keepaliveInterval
|
conf.KeepaliveInterval = keepaliveInterval
|
||||||
conf.ManagerInterval = managerInterval
|
conf.ManagerInterval = managerInterval
|
||||||
conf.DisallowedTopics = disallowedTopics
|
conf.DisallowedTopics = disallowedTopics
|
||||||
conf.WebRoot = webRoot
|
conf.WebRootIsApp = webRootIsApp
|
||||||
conf.UpstreamBaseURL = upstreamBaseURL
|
conf.UpstreamBaseURL = upstreamBaseURL
|
||||||
conf.UpstreamAccessToken = upstreamAccessToken
|
|
||||||
conf.SMTPSenderAddr = smtpSenderAddr
|
conf.SMTPSenderAddr = smtpSenderAddr
|
||||||
conf.SMTPSenderUser = smtpSenderUser
|
conf.SMTPSenderUser = smtpSenderUser
|
||||||
conf.SMTPSenderPass = smtpSenderPass
|
conf.SMTPSenderPass = smtpSenderPass
|
||||||
|
@ -333,10 +291,6 @@ func execServe(c *cli.Context) error {
|
||||||
conf.SMTPServerListen = smtpServerListen
|
conf.SMTPServerListen = smtpServerListen
|
||||||
conf.SMTPServerDomain = smtpServerDomain
|
conf.SMTPServerDomain = smtpServerDomain
|
||||||
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
|
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
|
||||||
conf.TwilioAccount = twilioAccount
|
|
||||||
conf.TwilioAuthToken = twilioAuthToken
|
|
||||||
conf.TwilioPhoneNumber = twilioPhoneNumber
|
|
||||||
conf.TwilioVerifyService = twilioVerifyService
|
|
||||||
conf.TotalTopicLimit = totalTopicLimit
|
conf.TotalTopicLimit = totalTopicLimit
|
||||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||||
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
||||||
|
@ -347,23 +301,14 @@ func execServe(c *cli.Context) error {
|
||||||
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
|
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
|
||||||
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||||
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||||
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
|
|
||||||
conf.BehindProxy = behindProxy
|
conf.BehindProxy = behindProxy
|
||||||
conf.StripeSecretKey = stripeSecretKey
|
conf.StripeSecretKey = stripeSecretKey
|
||||||
conf.StripeWebhookKey = stripeWebhookKey
|
conf.StripeWebhookKey = stripeWebhookKey
|
||||||
conf.BillingContact = billingContact
|
conf.EnableWeb = enableWeb
|
||||||
conf.EnableSignup = enableSignup
|
conf.EnableSignup = enableSignup
|
||||||
conf.EnableLogin = enableLogin
|
conf.EnableLogin = enableLogin
|
||||||
conf.EnableReservations = enableReservations
|
conf.EnableReservations = enableReservations
|
||||||
conf.EnableMetrics = enableMetrics
|
|
||||||
conf.MetricsListenHTTP = metricsListenHTTP
|
|
||||||
conf.ProfileListenHTTP = profileListenHTTP
|
|
||||||
conf.Version = c.App.Version
|
conf.Version = c.App.Version
|
||||||
conf.WebPushPrivateKey = webPushPrivateKey
|
|
||||||
conf.WebPushPublicKey = webPushPublicKey
|
|
||||||
conf.WebPushFile = webPushFile
|
|
||||||
conf.WebPushEmailAddress = webPushEmailAddress
|
|
||||||
conf.WebPushStartupQueries = webPushStartupQueries
|
|
||||||
|
|
||||||
// Set up hot-reloading of config
|
// Set up hot-reloading of config
|
||||||
go sigHandlerConfigReload(config)
|
go sigHandlerConfigReload(config)
|
||||||
|
|
|
@ -9,12 +9,12 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -3,10 +3,10 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/client"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/log"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
@ -30,7 +30,6 @@ var flagsSubscribe = append(
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
&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: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
|
||||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, 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.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token 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: "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: "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"},
|
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
|
||||||
|
@ -72,7 +71,7 @@ ntfy subscribe TOPIC COMMAND
|
||||||
$NTFY_TITLE $title, $t Message title
|
$NTFY_TITLE $title, $t Message title
|
||||||
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
|
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
|
||||||
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
|
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
|
||||||
$NTFY_RAW $raw Raw JSON message
|
$NTFY_RAW $raw Raw JSON message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
|
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
|
||||||
|
@ -98,18 +97,11 @@ func execSubscribe(c *cli.Context) error {
|
||||||
cl := client.New(conf)
|
cl := client.New(conf)
|
||||||
since := c.String("since")
|
since := c.String("since")
|
||||||
user := c.String("user")
|
user := c.String("user")
|
||||||
token := c.String("token")
|
|
||||||
poll := c.Bool("poll")
|
poll := c.Bool("poll")
|
||||||
scheduled := c.Bool("scheduled")
|
scheduled := c.Bool("scheduled")
|
||||||
fromConfig := c.Bool("from-config")
|
fromConfig := c.Bool("from-config")
|
||||||
topic := c.Args().Get(0)
|
topic := c.Args().Get(0)
|
||||||
command := c.Args().Get(1)
|
command := c.Args().Get(1)
|
||||||
|
|
||||||
// Checks
|
|
||||||
if user != "" && token != "" {
|
|
||||||
return errors.New("cannot set both --user and --token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fromConfig {
|
if !fromConfig {
|
||||||
conf.Subscribe = nil // wipe if --from-config not passed
|
conf.Subscribe = nil // wipe if --from-config not passed
|
||||||
}
|
}
|
||||||
|
@ -117,9 +109,7 @@ func execSubscribe(c *cli.Context) error {
|
||||||
if since != "" {
|
if since != "" {
|
||||||
options = append(options, client.WithSince(since))
|
options = append(options, client.WithSince(since))
|
||||||
}
|
}
|
||||||
if token != "" {
|
if user != "" {
|
||||||
options = append(options, client.WithBearerAuth(token))
|
|
||||||
} else if user != "" {
|
|
||||||
var pass string
|
var pass string
|
||||||
parts := strings.SplitN(user, ":", 2)
|
parts := strings.SplitN(user, ":", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
|
@ -135,10 +125,9 @@ func execSubscribe(c *cli.Context) error {
|
||||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||||
}
|
}
|
||||||
options = append(options, client.WithBasicAuth(user, pass))
|
options = append(options, client.WithBasicAuth(user, pass))
|
||||||
} else if conf.DefaultToken != "" {
|
}
|
||||||
options = append(options, client.WithBearerAuth(conf.DefaultToken))
|
if poll {
|
||||||
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
options = append(options, client.WithPoll())
|
||||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
|
||||||
}
|
}
|
||||||
if scheduled {
|
if scheduled {
|
||||||
options = append(options, client.WithScheduled())
|
options = append(options, client.WithScheduled())
|
||||||
|
@ -156,9 +145,6 @@ func execSubscribe(c *cli.Context) error {
|
||||||
|
|
||||||
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
||||||
for _, s := range conf.Subscribe { // may be nil
|
for _, s := range conf.Subscribe { // may be nil
|
||||||
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
|
||||||
options = append(options, auth)
|
|
||||||
}
|
|
||||||
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
|
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -189,15 +175,22 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
for filter, value := range s.If {
|
for filter, value := range s.If {
|
||||||
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
||||||
}
|
}
|
||||||
|
var user string
|
||||||
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
var password *string
|
||||||
topicOptions = append(topicOptions, auth)
|
if s.User != "" {
|
||||||
|
user = s.User
|
||||||
|
} else if conf.DefaultUser != "" {
|
||||||
|
user = conf.DefaultUser
|
||||||
}
|
}
|
||||||
|
if s.Password != nil {
|
||||||
subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
|
password = s.Password
|
||||||
if err != nil {
|
} else if conf.DefaultPassword != nil {
|
||||||
return err
|
password = conf.DefaultPassword
|
||||||
}
|
}
|
||||||
|
if user != "" && password != nil {
|
||||||
|
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
|
||||||
|
}
|
||||||
|
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||||
if s.Command != "" {
|
if s.Command != "" {
|
||||||
cmds[subscriptionID] = s.Command
|
cmds[subscriptionID] = s.Command
|
||||||
} else if conf.DefaultCommand != "" {
|
} else if conf.DefaultCommand != "" {
|
||||||
|
@ -207,10 +200,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if topic != "" {
|
if topic != "" {
|
||||||
subscriptionID, err := cl.Subscribe(topic, options...)
|
subscriptionID := cl.Subscribe(topic, options...)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cmds[subscriptionID] = command
|
cmds[subscriptionID] = command
|
||||||
}
|
}
|
||||||
for m := range cl.Messages {
|
for m := range cl.Messages {
|
||||||
|
@ -224,30 +214,6 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
|
||||||
// if an explicit empty token or empty user:pass is given, exit without auth
|
|
||||||
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
|
|
||||||
return client.WithEmptyAuth()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for subscription token then subscription user:pass
|
|
||||||
if s.Token != nil && *s.Token != "" {
|
|
||||||
return client.WithBearerAuth(*s.Token)
|
|
||||||
}
|
|
||||||
if s.User != nil && *s.User != "" && s.Password != nil {
|
|
||||||
return client.WithBasicAuth(*s.User, *s.Password)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
|
||||||
if conf.DefaultToken != "" {
|
|
||||||
return client.WithBearerAuth(conf.DefaultToken)
|
|
||||||
}
|
|
||||||
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
|
||||||
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
|
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
|
||||||
if command != "" {
|
if command != "" {
|
||||||
runCommand(c, command, m)
|
runCommand(c, command, m)
|
||||||
|
|
|
@ -1,417 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
user: philipp
|
|
||||||
password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: fake
|
|
||||||
default-password: password
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
user: philipp
|
|
||||||
password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
user: philipp
|
|
||||||
password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_FAKETOKEN0123456789FAKETOKEN
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
|
|
||||||
app, _, _, _ := newTestApp()
|
|
||||||
err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Equal(t, "cannot set both --user and --token", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Default_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-user: philipp
|
|
||||||
default-password: mypass
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
user: ""
|
|
||||||
password: ""
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) {
|
|
||||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
|
||||||
require.Equal(t, "", r.Header.Get("Authorization"))
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
w.Write([]byte(message))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
|
||||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
|
||||||
default-host: %s
|
|
||||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
|
||||||
subscribe:
|
|
||||||
- topic: mytopic
|
|
||||||
token: ""
|
|
||||||
`, server.URL)), 0600))
|
|
||||||
|
|
||||||
app, _, stdout, _ := newTestApp()
|
|
||||||
|
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
|
||||||
|
|
||||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
|
||||||
}
|
|
83
cmd/tier.go
|
@ -5,9 +5,10 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -16,13 +17,12 @@ func init() {
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultMessageLimit = 5000
|
defaultMessageLimit = 5000
|
||||||
defaultMessageExpiryDuration = "12h"
|
defaultMessageExpiryDuration = 12 * time.Hour
|
||||||
defaultEmailLimit = 20
|
defaultEmailLimit = 20
|
||||||
defaultCallLimit = 0
|
|
||||||
defaultReservationLimit = 3
|
defaultReservationLimit = 3
|
||||||
defaultAttachmentFileSizeLimit = "15M"
|
defaultAttachmentFileSizeLimit = "15M"
|
||||||
defaultAttachmentTotalSizeLimit = "100M"
|
defaultAttachmentTotalSizeLimit = "100M"
|
||||||
defaultAttachmentExpiryDuration = "6h"
|
defaultAttachmentExpiryDuration = 6 * time.Hour
|
||||||
defaultAttachmentBandwidthLimit = "1G"
|
defaultAttachmentBandwidthLimit = "1G"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -47,16 +47,14 @@ var cmdTier = &cli.Command{
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{Name: "name", Usage: "tier name"},
|
&cli.StringFlag{Name: "name", Usage: "tier name"},
|
||||||
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
|
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
|
||||||
&cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
|
&cli.DurationFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
|
||||||
&cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
|
&cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
|
||||||
&cli.Int64Flag{Name: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"},
|
|
||||||
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
|
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
|
||||||
&cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
|
&cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
|
||||||
&cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
|
&cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
|
||||||
&cli.StringFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
|
&cli.DurationFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
|
||||||
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
||||||
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
|
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
||||||
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
|
|
||||||
&cli.BoolFlag{Name: "ignore-exists", Usage: "if the tier already exists, perform no action and exit"},
|
&cli.BoolFlag{Name: "ignore-exists", Usage: "if the tier already exists, perform no action and exit"},
|
||||||
},
|
},
|
||||||
Description: `Add a new tier to the ntfy user database.
|
Description: `Add a new tier to the ntfy user database.
|
||||||
|
@ -91,16 +89,14 @@ Examples:
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{Name: "name", Usage: "tier name"},
|
&cli.StringFlag{Name: "name", Usage: "tier name"},
|
||||||
&cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
|
&cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
|
||||||
&cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
|
&cli.DurationFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
|
||||||
&cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
|
&cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
|
||||||
&cli.Int64Flag{Name: "call-limit", Usage: "daily phone call limit"},
|
|
||||||
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
|
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
|
||||||
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
|
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
|
||||||
&cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
|
&cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
|
||||||
&cli.StringFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
|
&cli.DurationFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
|
||||||
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
|
||||||
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
|
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
|
||||||
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
|
|
||||||
},
|
},
|
||||||
Description: `Updates a tier to change the limits.
|
Description: `Updates a tier to change the limits.
|
||||||
|
|
||||||
|
@ -114,8 +110,7 @@ Examples:
|
||||||
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
ntfy tier change --name="Pro" pro # Update the name of an existing tier
|
||||||
ntfy tier change \ # Update multiple limits and fields
|
ntfy tier change \ # Update multiple limits and fields
|
||||||
--message-expiry-duration=24h \
|
--message-expiry-duration=24h \
|
||||||
--stripe-monthly-price-id=price_1234 \
|
--stripe-price-id=price_1234 \
|
||||||
--stripe-monthly-price-id=price_5678 \
|
|
||||||
pro
|
pro
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
@ -171,10 +166,6 @@ func execTierAdd(c *cli.Context) error {
|
||||||
return errors.New("tier code expected, type 'ntfy tier add --help' for help")
|
return errors.New("tier code expected, type 'ntfy tier add --help' for help")
|
||||||
} else if !user.AllowedTier(code) {
|
} else if !user.AllowedTier(code) {
|
||||||
return errors.New("tier code must consist only of numbers and letters")
|
return errors.New("tier code must consist only of numbers and letters")
|
||||||
} else if c.String("stripe-monthly-price-id") != "" && c.String("stripe-yearly-price-id") == "" {
|
|
||||||
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
|
|
||||||
} else if c.String("stripe-monthly-price-id") == "" && c.String("stripe-yearly-price-id") != "" {
|
|
||||||
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
|
|
||||||
}
|
}
|
||||||
manager, err := createUserManager(c)
|
manager, err := createUserManager(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -191,10 +182,6 @@ func execTierAdd(c *cli.Context) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = code
|
name = code
|
||||||
}
|
}
|
||||||
messageExpiryDuration, err := util.ParseDuration(c.String("message-expiry-duration"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit"))
|
attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -207,25 +194,19 @@ func execTierAdd(c *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
attachmentExpiryDuration, err := util.ParseDuration(c.String("attachment-expiry-duration"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tier := &user.Tier{
|
tier := &user.Tier{
|
||||||
ID: "", // Generated
|
ID: "", // Generated
|
||||||
Code: code,
|
Code: code,
|
||||||
Name: name,
|
Name: name,
|
||||||
MessageLimit: c.Int64("message-limit"),
|
MessageLimit: c.Int64("message-limit"),
|
||||||
MessageExpiryDuration: messageExpiryDuration,
|
MessageExpiryDuration: c.Duration("message-expiry-duration"),
|
||||||
EmailLimit: c.Int64("email-limit"),
|
EmailLimit: c.Int64("email-limit"),
|
||||||
CallLimit: c.Int64("call-limit"),
|
|
||||||
ReservationLimit: c.Int64("reservation-limit"),
|
ReservationLimit: c.Int64("reservation-limit"),
|
||||||
AttachmentFileSizeLimit: attachmentFileSizeLimit,
|
AttachmentFileSizeLimit: attachmentFileSizeLimit,
|
||||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
|
AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
|
||||||
AttachmentExpiryDuration: attachmentExpiryDuration,
|
AttachmentExpiryDuration: c.Duration("attachment-expiry-duration"),
|
||||||
AttachmentBandwidthLimit: attachmentBandwidthLimit,
|
AttachmentBandwidthLimit: attachmentBandwidthLimit,
|
||||||
StripeMonthlyPriceID: c.String("stripe-monthly-price-id"),
|
StripePriceID: c.String("stripe-price-id"),
|
||||||
StripeYearlyPriceID: c.String("stripe-yearly-price-id"),
|
|
||||||
}
|
}
|
||||||
if err := manager.AddTier(tier); err != nil {
|
if err := manager.AddTier(tier); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -263,17 +244,11 @@ func execTierChange(c *cli.Context) error {
|
||||||
tier.MessageLimit = c.Int64("message-limit")
|
tier.MessageLimit = c.Int64("message-limit")
|
||||||
}
|
}
|
||||||
if c.IsSet("message-expiry-duration") {
|
if c.IsSet("message-expiry-duration") {
|
||||||
tier.MessageExpiryDuration, err = util.ParseDuration(c.String("message-expiry-duration"))
|
tier.MessageExpiryDuration = c.Duration("message-expiry-duration")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if c.IsSet("email-limit") {
|
if c.IsSet("email-limit") {
|
||||||
tier.EmailLimit = c.Int64("email-limit")
|
tier.EmailLimit = c.Int64("email-limit")
|
||||||
}
|
}
|
||||||
if c.IsSet("call-limit") {
|
|
||||||
tier.CallLimit = c.Int64("call-limit")
|
|
||||||
}
|
|
||||||
if c.IsSet("reservation-limit") {
|
if c.IsSet("reservation-limit") {
|
||||||
tier.ReservationLimit = c.Int64("reservation-limit")
|
tier.ReservationLimit = c.Int64("reservation-limit")
|
||||||
}
|
}
|
||||||
|
@ -290,10 +265,7 @@ func execTierChange(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.IsSet("attachment-expiry-duration") {
|
if c.IsSet("attachment-expiry-duration") {
|
||||||
tier.AttachmentExpiryDuration, err = util.ParseDuration(c.String("attachment-expiry-duration"))
|
tier.AttachmentExpiryDuration = c.Duration("attachment-expiry-duration")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if c.IsSet("attachment-bandwidth-limit") {
|
if c.IsSet("attachment-bandwidth-limit") {
|
||||||
tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit"))
|
tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit"))
|
||||||
|
@ -301,16 +273,8 @@ func execTierChange(c *cli.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.IsSet("stripe-monthly-price-id") {
|
if c.IsSet("stripe-price-id") {
|
||||||
tier.StripeMonthlyPriceID = c.String("stripe-monthly-price-id")
|
tier.StripePriceID = c.String("stripe-price-id")
|
||||||
}
|
|
||||||
if c.IsSet("stripe-yearly-price-id") {
|
|
||||||
tier.StripeYearlyPriceID = c.String("stripe-yearly-price-id")
|
|
||||||
}
|
|
||||||
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID == "" {
|
|
||||||
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
|
|
||||||
} else if tier.StripeMonthlyPriceID == "" && tier.StripeYearlyPriceID != "" {
|
|
||||||
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
|
|
||||||
}
|
}
|
||||||
if err := manager.UpdateTier(tier); err != nil {
|
if err := manager.UpdateTier(tier); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -355,20 +319,19 @@ func execTierList(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func printTier(c *cli.Context, tier *user.Tier) {
|
func printTier(c *cli.Context, tier *user.Tier) {
|
||||||
prices := "(none)"
|
stripePriceID := tier.StripePriceID
|
||||||
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID != "" {
|
if stripePriceID == "" {
|
||||||
prices = fmt.Sprintf("%s / %s", tier.StripeMonthlyPriceID, tier.StripeYearlyPriceID)
|
stripePriceID = "(none)"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
|
fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
|
fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
|
fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
|
fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
|
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit)
|
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
|
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
|
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
|
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
|
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
|
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
|
fmt.Fprintf(c.App.ErrWriter, "- Stripe price: %s\n", stripePriceID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,25 +29,24 @@ func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
|
||||||
app, _, _, stderr = newTestApp()
|
app, _, _, stderr = newTestApp()
|
||||||
require.Nil(t, runTierCommand(app, conf, "change",
|
require.Nil(t, runTierCommand(app, conf, "change",
|
||||||
"--message-limit=999",
|
"--message-limit=999",
|
||||||
"--message-expiry-duration=2d",
|
"--message-expiry-duration=99h",
|
||||||
"--email-limit=91",
|
"--email-limit=91",
|
||||||
"--reservation-limit=98",
|
"--reservation-limit=98",
|
||||||
"--attachment-file-size-limit=100m",
|
"--attachment-file-size-limit=100m",
|
||||||
"--attachment-expiry-duration=1d",
|
"--attachment-expiry-duration=7h",
|
||||||
"--attachment-total-size-limit=10G",
|
"--attachment-total-size-limit=10G",
|
||||||
"--attachment-bandwidth-limit=100G",
|
"--attachment-bandwidth-limit=100G",
|
||||||
"--stripe-monthly-price-id=price_991",
|
"--stripe-price-id=price_991",
|
||||||
"--stripe-yearly-price-id=price_992",
|
|
||||||
"pro",
|
"pro",
|
||||||
))
|
))
|
||||||
require.Contains(t, stderr.String(), "- Message limit: 999")
|
require.Contains(t, stderr.String(), "- Message limit: 999")
|
||||||
require.Contains(t, stderr.String(), "- Message expiry duration: 48h")
|
require.Contains(t, stderr.String(), "- Message expiry duration: 99h")
|
||||||
require.Contains(t, stderr.String(), "- Email limit: 91")
|
require.Contains(t, stderr.String(), "- Email limit: 91")
|
||||||
require.Contains(t, stderr.String(), "- Reservation limit: 98")
|
require.Contains(t, stderr.String(), "- Reservation limit: 98")
|
||||||
require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
|
require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
|
||||||
require.Contains(t, stderr.String(), "- Attachment expiry duration: 24h")
|
require.Contains(t, stderr.String(), "- Attachment expiry duration: 7h")
|
||||||
require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
|
require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
|
||||||
require.Contains(t, stderr.String(), "- Stripe prices (monthly/yearly): price_991 / price_992")
|
require.Contains(t, stderr.String(), "- Stripe price: price_991")
|
||||||
|
|
||||||
app, _, _, stderr = newTestApp()
|
app, _, _, stderr = newTestApp()
|
||||||
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
|
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
|
||||||
|
|
|
@ -5,9 +5,9 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,10 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
"heckel.io/ntfy/user"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/test"
|
|
||||||
"git.zio.sh/astra/ntfy/v2/user"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
|
"heckel.io/ntfy/user"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
//go:build !noserver
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/SherClockHolmes/webpush-go"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commands = append(commands, cmdWebPush)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmdWebPush = &cli.Command{
|
|
||||||
Name: "webpush",
|
|
||||||
Usage: "Generate keys, in the future manage web push subscriptions",
|
|
||||||
UsageText: "ntfy webpush [keys]",
|
|
||||||
Category: categoryServer,
|
|
||||||
|
|
||||||
Subcommands: []*cli.Command{
|
|
||||||
{
|
|
||||||
Action: generateWebPushKeys,
|
|
||||||
Name: "keys",
|
|
||||||
Usage: "Generate VAPID keys to enable browser background push notifications",
|
|
||||||
UsageText: "ntfy webpush keys",
|
|
||||||
Category: categoryServer,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateWebPushKeys(c *cli.Context) error {
|
|
||||||
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
|
|
||||||
|
|
||||||
web-push-public-key: %s
|
|
||||||
web-push-private-key: %s
|
|
||||||
web-push-file: /var/cache/ntfy/webpush.db # or similar
|
|
||||||
web-push-email-address: <email address>
|
|
||||||
|
|
||||||
See https://ntfy.sh/docs/config/#web-push for details.
|
|
||||||
`, publicKey, privateKey)
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.zio.sh/astra/ntfy/v2/server"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
|
|
||||||
app, _, _, stderr := newTestApp()
|
|
||||||
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
|
|
||||||
require.Contains(t, stderr.String(), "Web Push keys generated.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
|
|
||||||
webPushArgs := []string{
|
|
||||||
"ntfy",
|
|
||||||
"--log-level=ERROR",
|
|
||||||
"webpush",
|
|
||||||
}
|
|
||||||
return app.Run(append(webPushArgs, args...))
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block announce %}
|
|
||||||
<style>
|
|
||||||
div[data-md-component="announce"] {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-md-component="announce"] a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-md-component="announce"] a:hover, div[data-md-component="announce"] a:focus {
|
|
||||||
transition: ease-in 150ms;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-md-component="announce"] .md-banner__button {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-md-component="announce"] .md-banner.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[data-md-component="announce"] .twemoji {
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<button id="announce-bar-close" class="md-banner__button md-icon" aria-label="Don't show this again">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
If you like ntfy, please consider sponsoring me via <a target="_blank" href="https://github.com/sponsors/binwiederhier"><strong>GitHub Sponsors</strong></a>
|
|
||||||
or <a target="_blank" href="https://en.liberapay.com/ntfy/"><strong>Liberapay</strong></a>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 36 36" class="twemoji md-footer-custom-text">
|
|
||||||
<path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/>
|
|
||||||
</svg>, or subscribing to <a target="_blank" href="https://ntfy.sh/app"><strong>ntfy Pro</strong></a>.
|
|
||||||
<script>
|
|
||||||
announceBarKey = 'announce-bar-closed-sponsor';
|
|
||||||
document.getElementById('announce-bar-close').addEventListener('click', (e) => {
|
|
||||||
localStorage.setItem(announceBarKey, 'true');
|
|
||||||
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
|
|
||||||
});
|
|
||||||
if (localStorage.getItem(announceBarKey) === 'true') {
|
|
||||||
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
248
docs/config.md
|
@ -44,14 +44,6 @@ Here are a few working sample configs:
|
||||||
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "server.yml (behind proxy, with cache + attachments)"
|
|
||||||
``` yaml
|
|
||||||
base-url: "http://ntfy.example.com"
|
|
||||||
listen-http: ":2586"
|
|
||||||
cache-file: "/var/cache/ntfy/cache.db"
|
|
||||||
attachment-cache-dir: "/var/cache/ntfy/attachments"
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "server.yml (ntfy.sh config)"
|
=== "server.yml (ntfy.sh config)"
|
||||||
``` yaml
|
``` yaml
|
||||||
# All the things: Behind a proxy, Firebase, cache, attachments,
|
# All the things: Behind a proxy, Firebase, cache, attachments,
|
||||||
|
@ -466,31 +458,6 @@ $ dig A mx1.ntfy.sh +short
|
||||||
3.139.215.220
|
3.139.215.220
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local-only email
|
|
||||||
If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
|
|
||||||
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
|
|
||||||
anything).
|
|
||||||
|
|
||||||
=== "/etc/ntfy/server.yml"
|
|
||||||
``` yaml
|
|
||||||
smtp-server-listen: ":25"
|
|
||||||
smtp-server-domain: "example.com"
|
|
||||||
smtp-server-addr-prefix: "ntfy-" # optional
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
|
|
||||||
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
|
|
||||||
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
|
|
||||||
In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
|
|
||||||
`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
|
|
||||||
|
|
||||||
So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
|
|
||||||
topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
|
|
||||||
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
|
|
||||||
|
|
||||||
If the internal service lets you use define an email "Subject", it will become the title of the notification.
|
|
||||||
The body of the email will become the message of the notification.
|
|
||||||
|
|
||||||
## Behind a proxy (TLS, etc.)
|
## Behind a proxy (TLS, etc.)
|
||||||
!!! warning
|
!!! warning
|
||||||
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
||||||
|
@ -682,8 +649,8 @@ or the root domain:
|
||||||
<VirtualHost *:80>
|
<VirtualHost *:80>
|
||||||
ServerName ntfy.sh
|
ServerName ntfy.sh
|
||||||
|
|
||||||
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
# Proxy connections to ntfy (requires "a2enmod proxy")
|
||||||
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
ProxyPass / http://127.0.0.1:2586/
|
||||||
ProxyPassReverse / http://127.0.0.1:2586/
|
ProxyPassReverse / http://127.0.0.1:2586/
|
||||||
|
|
||||||
SetEnv proxy-nokeepalive 1
|
SetEnv proxy-nokeepalive 1
|
||||||
|
@ -692,12 +659,18 @@ or the root domain:
|
||||||
# Higher than the max message size of 4096 bytes
|
# Higher than the max message size of 4096 bytes
|
||||||
LimitRequestBody 102400
|
LimitRequestBody 102400
|
||||||
|
|
||||||
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
|
# Enable mod_rewrite (requires "a2enmod rewrite")
|
||||||
# it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
|
RewriteEngine on
|
||||||
<If "%{REQUEST_METHOD} == 'GET'">
|
|
||||||
RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
|
|
||||||
</If>
|
|
||||||
|
|
||||||
|
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
||||||
|
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||||
|
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||||
|
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
|
||||||
|
# it to work with curl without the annoying https:// prefix
|
||||||
|
RewriteCond %{REQUEST_METHOD} GET
|
||||||
|
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
|
|
||||||
<VirtualHost *:443>
|
<VirtualHost *:443>
|
||||||
|
@ -708,8 +681,8 @@ or the root domain:
|
||||||
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
|
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
|
||||||
Include /etc/letsencrypt/options-ssl-apache.conf
|
Include /etc/letsencrypt/options-ssl-apache.conf
|
||||||
|
|
||||||
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
|
# Proxy connections to ntfy (requires "a2enmod proxy")
|
||||||
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
|
ProxyPass / http://127.0.0.1:2586/
|
||||||
ProxyPassReverse / http://127.0.0.1:2586/
|
ProxyPassReverse / http://127.0.0.1:2586/
|
||||||
|
|
||||||
SetEnv proxy-nokeepalive 1
|
SetEnv proxy-nokeepalive 1
|
||||||
|
@ -718,6 +691,13 @@ or the root domain:
|
||||||
# Higher than the max message size of 4096 bytes
|
# Higher than the max message size of 4096 bytes
|
||||||
LimitRequestBody 102400
|
LimitRequestBody 102400
|
||||||
|
|
||||||
|
# Enable mod_rewrite (requires "a2enmod rewrite")
|
||||||
|
RewriteEngine on
|
||||||
|
|
||||||
|
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
|
||||||
|
RewriteCond %{HTTP:Upgrade} websocket [NC]
|
||||||
|
RewriteCond %{HTTP:Connection} upgrade [NC]
|
||||||
|
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
|
||||||
</VirtualHost>
|
</VirtualHost>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -779,7 +759,6 @@ To configure it, simply set `upstream-base-url` like so:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
upstream-base-url: "https://ntfy.sh"
|
upstream-base-url: "https://ntfy.sh"
|
||||||
upstream-access-token: "..." # optional, only if rate limits exceeded, or upstream server protected
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If set, all incoming messages will publish a poll request to the configured upstream server, containing
|
If set, all incoming messages will publish a poll request to the configured upstream server, containing
|
||||||
|
@ -809,57 +788,6 @@ Note that the self-hosted server literally sends the message `New message` for e
|
||||||
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
|
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
|
||||||
it'll show `New message` as a popup.
|
it'll show `New message` as a popup.
|
||||||
|
|
||||||
## Web Push
|
|
||||||
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
|
|
||||||
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
|
|
||||||
When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the
|
|
||||||
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
|
|
||||||
forward it to the browser.
|
|
||||||
|
|
||||||
To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
|
|
||||||
a database to keep track of the browser's subscriptions, and an admin email address (you):
|
|
||||||
|
|
||||||
- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
|
|
||||||
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
|
|
||||||
- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
|
|
||||||
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
|
|
||||||
- `web-push-startup-queries` is an optional list of queries to run on startup`
|
|
||||||
|
|
||||||
Limitations:
|
|
||||||
|
|
||||||
- Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A _valid_
|
|
||||||
certificate is required, as service workers will not run on origins with untrusted certificates.
|
|
||||||
|
|
||||||
- Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This
|
|
||||||
is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.
|
|
||||||
|
|
||||||
To configure VAPID keys, first generate them:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ ntfy webpush keys
|
|
||||||
Web Push keys generated.
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
|
|
||||||
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
|
|
||||||
web-push-file: /var/cache/ntfy/webpush.db
|
|
||||||
web-push-email-address: sysadmin@example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days,
|
|
||||||
and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
|
|
||||||
subscriptions are also removed automatically.
|
|
||||||
|
|
||||||
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
|
|
||||||
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.
|
|
||||||
|
|
||||||
Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and
|
|
||||||
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.
|
|
||||||
|
|
||||||
## Tiers
|
## Tiers
|
||||||
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
|
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
|
||||||
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
|
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
|
||||||
|
@ -886,7 +814,6 @@ ntfy tier add \
|
||||||
--message-limit=10000 \
|
--message-limit=10000 \
|
||||||
--message-expiry-duration=24h \
|
--message-expiry-duration=24h \
|
||||||
--email-limit=50 \
|
--email-limit=50 \
|
||||||
--call-limit=10 \
|
|
||||||
--reservation-limit=10 \
|
--reservation-limit=10 \
|
||||||
--attachment-file-size-limit=100M \
|
--attachment-file-size-limit=100M \
|
||||||
--attachment-total-size-limit=1G \
|
--attachment-total-size-limit=1G \
|
||||||
|
@ -912,8 +839,6 @@ config options:
|
||||||
enables payments in the ntfy web app (e.g. Upgrade dialog). See [API keys](https://dashboard.stripe.com/apikeys).
|
enables payments in the ntfy web app (e.g. Upgrade dialog). See [API keys](https://dashboard.stripe.com/apikeys).
|
||||||
* `stripe-webhook-key` is the key required to validate the authenticity of incoming webhooks from Stripe.
|
* `stripe-webhook-key` is the key required to validate the authenticity of incoming webhooks from Stripe.
|
||||||
Webhooks are essential to keep the local database in sync with the payment provider. See [Webhooks](https://dashboard.stripe.com/webhooks).
|
Webhooks are essential to keep the local database in sync with the payment provider. See [Webhooks](https://dashboard.stripe.com/webhooks).
|
||||||
* `billing-contact` is an email address or website displayed in the "Upgrade tier" dialog to let people reach
|
|
||||||
out with billing questions. If unset, nothing will be displayed.
|
|
||||||
|
|
||||||
In addition to setting these two options, you also need to define a [Stripe webhook](https://dashboard.stripe.com/webhooks)
|
In addition to setting these two options, you also need to define a [Stripe webhook](https://dashboard.stripe.com/webhooks)
|
||||||
for the `customer.subscription.updated` and `customer.subscription.deleted` event, which points
|
for the `customer.subscription.updated` and `customer.subscription.deleted` event, which points
|
||||||
|
@ -924,25 +849,8 @@ Here's an example:
|
||||||
``` yaml
|
``` yaml
|
||||||
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
|
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
|
||||||
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
|
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
|
||||||
billing-contact: "phil@example.com"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Phone calls
|
|
||||||
ntfy supports phone calls via [Twilio](https://www.twilio.com/) as a call provider. If phone calls are enabled,
|
|
||||||
users can verify and add a phone number, and then receive phone calls when publishing a message using the `X-Call` header.
|
|
||||||
See [publishing page](publish.md#phone-calls) for more details.
|
|
||||||
|
|
||||||
To enable Twilio integration, sign up with [Twilio](https://www.twilio.com/), purchase a phone number (Toll free numbers
|
|
||||||
are the easiest), and then configure the following options:
|
|
||||||
|
|
||||||
* `twilio-account` is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
|
|
||||||
* `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
|
|
||||||
* `twilio-phone-number` is the outgoing phone number you purchased, e.g. +18775132586
|
|
||||||
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
|
|
||||||
|
|
||||||
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
|
|
||||||
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
|
|
||||||
|
|
||||||
## Rate limiting
|
## Rate limiting
|
||||||
!!! info
|
!!! info
|
||||||
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
||||||
|
@ -1021,25 +929,6 @@ If this ever happens, there will be a log message that looks something like this
|
||||||
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor
|
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor
|
||||||
```
|
```
|
||||||
|
|
||||||
### Subscriber-based rate limiting
|
|
||||||
By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment
|
|
||||||
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
|
|
||||||
of a topic's subscriber, instead of the limits of the publisher.**
|
|
||||||
|
|
||||||
If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
|
|
||||||
to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
|
|
||||||
publishers (e.g. Matrix/Mastodon servers) are allowed to send.
|
|
||||||
|
|
||||||
Once enabled, a client may send a `Rate-Topics: <topic1>,<topic2>,...` header when subscribing to topics via
|
|
||||||
HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
|
|
||||||
to use when publishing on this topic. Note that setting the rate visitor requires **read-write permission** on the topic.
|
|
||||||
|
|
||||||
UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
|
|
||||||
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
|
|
||||||
`visitor-message-daily-limit`.
|
|
||||||
|
|
||||||
To enable subscriber-based rate limiting, set `visitor-subscriber-rate-limiting: true`.
|
|
||||||
|
|
||||||
## Tuning for scale
|
## Tuning for scale
|
||||||
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
|
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
|
||||||
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
|
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
|
||||||
|
@ -1178,60 +1067,6 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||||
maxretry = 10
|
maxretry = 10
|
||||||
```
|
```
|
||||||
|
|
||||||
## Health checks
|
|
||||||
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
|
|
||||||
If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"healthy":true}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to
|
|
||||||
create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)).
|
|
||||||
|
|
||||||
To configure the metrics endpoint, either set `enable-metrics` and/or set the `listen-metrics-http` option to a dedicated
|
|
||||||
listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are
|
|
||||||
doing, and/or secure access to the endpoint in your reverse proxy.
|
|
||||||
|
|
||||||
- `enable-metrics` enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)
|
|
||||||
- `metrics-listen-http` exposes the metrics endpoint via a dedicated `[IP]:port`. If set, this option implicitly
|
|
||||||
enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
|
|
||||||
|
|
||||||
=== "server.yml (Using default port)"
|
|
||||||
```yaml
|
|
||||||
enable-metrics: true
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "server.yml (Using dedicated IP/port)"
|
|
||||||
```yaml
|
|
||||||
metrics-listen-http: "10.0.1.1:9090"
|
|
||||||
```
|
|
||||||
|
|
||||||
In Prometheus, an example scrape config would look like this:
|
|
||||||
|
|
||||||
=== "prometheus.yml"
|
|
||||||
```yaml
|
|
||||||
scrape_configs:
|
|
||||||
- job_name: "ntfy"
|
|
||||||
static_configs:
|
|
||||||
- targets: ["10.0.1.1:9090"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on GitHub](https://raw.githubusercontent.com/binwiederhier/ntfy/main/examples/grafana-dashboard/ntfy-grafana.json)):
|
|
||||||
|
|
||||||
<figure markdown style="padding-left: 50px; padding-right: 50px">
|
|
||||||
<a href="../../static/img/grafana-dashboard.png" target="_blank"><img src="../../static/img/grafana-dashboard.png"/></a>
|
|
||||||
<figcaption>ntfy Grafana dashboard</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
## Profiling
|
|
||||||
ntfy can expose Go's [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoints to support profiling of the ntfy server.
|
|
||||||
If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on `http://<ip>:<port>/debug/pprof/`.
|
|
||||||
This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the `profile-listen-http` config option.
|
|
||||||
|
|
||||||
## Logging & debugging
|
## Logging & debugging
|
||||||
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
|
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
|
||||||
|
|
||||||
|
@ -1330,15 +1165,10 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `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-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-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` | *string* | - | 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-` |
|
||||||
| `twilio-account` | `NTFY_TWILIO_ACCOUNT` | *string* | - | Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586 |
|
|
||||||
| `twilio-auth-token` | `NTFY_TWILIO_AUTH_TOKEN` | *string* | - | Twilio auth token, e.g. affebeef258625862586258625862586 |
|
|
||||||
| `twilio-phone-number` | `NTFY_TWILIO_PHONE_NUMBER` | *string* | - | Twilio outgoing phone number, e.g. +18775132586 |
|
|
||||||
| `twilio-verify-service` | `NTFY_TWILIO_VERIFY_SERVICE` | *string* | - | Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 |
|
|
||||||
| `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. |
|
| `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. |
|
| `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 |
|
| `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 |
|
||||||
| `upstream-access-token` | `NTFY_UPSTREAM_ACCESS_TOKEN` | *string* | `tk_zyYLYj...` | Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth |
|
|
||||||
| `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`. |
|
| `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`. |
|
||||||
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
|
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
|
||||||
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
|
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
|
||||||
|
@ -1348,25 +1178,19 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
|
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
|
||||||
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
|
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
|
||||||
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
||||||
| `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting |
|
| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) |
|
||||||
| `web-root` | `NTFY_WEB_ROOT` | *path*, e.g. `/` or `/app`, or `disable` | `/` | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable) |
|
|
||||||
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
|
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
|
||||||
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
|
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
|
||||||
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
|
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
|
||||||
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
|
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
|
||||||
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
|
||||||
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
|
|
||||||
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate |
|
|
||||||
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate |
|
|
||||||
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
|
|
||||||
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
|
|
||||||
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
|
|
||||||
|
|
||||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
```
|
```
|
||||||
|
$ ntfy serve --help
|
||||||
NAME:
|
NAME:
|
||||||
ntfy serve - Run the ntfy server
|
ntfy serve - Run the ntfy server
|
||||||
|
|
||||||
|
@ -1396,8 +1220,8 @@ OPTIONS:
|
||||||
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
|
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
|
||||||
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
||||||
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
||||||
--listen-http value, --listen_http value, -l value ip:port used as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
--listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
||||||
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
--listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
||||||
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
|
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
|
||||||
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
|
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
|
||||||
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
|
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
|
||||||
|
@ -1418,12 +1242,11 @@ OPTIONS:
|
||||||
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
|
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
|
||||||
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
||||||
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
|
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
|
||||||
--web-root value, --web_root value sets root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
|
--web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT]
|
||||||
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
||||||
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
|
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
|
||||||
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
|
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
|
||||||
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
|
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
|
||||||
--upstream-access-token value, --upstream_access_token value access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN]
|
|
||||||
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
|
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
|
||||||
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
|
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
|
||||||
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
|
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
|
||||||
|
@ -1431,10 +1254,6 @@ OPTIONS:
|
||||||
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
|
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
|
||||||
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
|
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
|
||||||
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
|
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
|
||||||
--twilio-account value, --twilio_account value Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT]
|
|
||||||
--twilio-auth-token value, --twilio_auth_token value Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
|
|
||||||
--twilio-phone-number value, --twilio_phone_number value Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
|
|
||||||
--twilio-verify-service value, --twilio_verify_service value Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
|
|
||||||
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
||||||
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
||||||
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||||
|
@ -1445,18 +1264,9 @@ OPTIONS:
|
||||||
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
|
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
|
||||||
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
||||||
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||||
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
|
|
||||||
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
||||||
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
|
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
|
||||||
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
|
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
|
||||||
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
|
--help, -h show help (default: false)
|
||||||
--enable-metrics, --enable_metrics if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS]
|
|
||||||
--metrics-listen-http value, --metrics_listen_http value ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP]
|
|
||||||
--profile-listen-http value, --profile_listen_http value ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP]
|
|
||||||
--web-push-public-key value, --web_push_public_key value public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY]
|
|
||||||
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
|
|
||||||
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
|
|
||||||
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
|
|
||||||
--web-push-startup-queries value, --web_push_startup-queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
|
||||||
--help, -h show help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ server consists of three components:
|
||||||
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
|
||||||
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
|
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
|
||||||
build the docs.
|
build the docs.
|
||||||
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/)
|
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
|
||||||
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
|
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
|
||||||
and install all the 100,000 dependencies (*sigh*).
|
and install all the 100,000 dependencies (*sigh*).
|
||||||
|
|
||||||
|
@ -163,15 +163,6 @@ $ make release-snapshot
|
||||||
|
|
||||||
During development, you may want to be more picky and build only certain things. Here are a few examples.
|
During development, you may want to be more picky and build only certain things. Here are a few examples.
|
||||||
|
|
||||||
### Build a Docker image only for Linux
|
|
||||||
|
|
||||||
This is useful to test the final build with web app, docs, and server without any dependencies locally
|
|
||||||
|
|
||||||
``` shell
|
|
||||||
$ make docker-dev
|
|
||||||
$ docker run --rm -p 80:80 binwiederhier/ntfy:dev serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build the ntfy binary
|
### Build the ntfy binary
|
||||||
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
|
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
|
||||||
|
|
||||||
|
@ -241,41 +232,6 @@ $ cd web
|
||||||
$ npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing Web Push locally
|
|
||||||
|
|
||||||
Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http>
|
|
||||||
|
|
||||||
#### With the dev servers
|
|
||||||
|
|
||||||
1. Get web push keys `go run main.go webpush keys`
|
|
||||||
|
|
||||||
2. Run the server with web push enabled
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go run main.go \
|
|
||||||
--log-level debug \
|
|
||||||
serve \
|
|
||||||
--web-push-public-key KEY \
|
|
||||||
--web-push-private-key KEY \
|
|
||||||
--web-push-email-address <email> \
|
|
||||||
--web-push-file=/tmp/webpush.db
|
|
||||||
```
|
|
||||||
|
|
||||||
3. In `web/public/config.js`:
|
|
||||||
|
|
||||||
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
|
|
||||||
|
|
||||||
- Set the `web_push_public_key` correctly.
|
|
||||||
|
|
||||||
4. Run `npm run start`
|
|
||||||
|
|
||||||
#### With a built package
|
|
||||||
|
|
||||||
1. Run `make web-build`
|
|
||||||
|
|
||||||
2. Run the server (step 2 above)
|
|
||||||
|
|
||||||
3. Open <http://localhost/>
|
|
||||||
### Build the docs
|
### Build the docs
|
||||||
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
|
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
|
||||||
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
|
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
|
||||||
|
@ -429,7 +385,7 @@ steps:
|
||||||
|
|
||||||
### XCode setup
|
### XCode setup
|
||||||
|
|
||||||
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
|
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
|
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
|
||||||
1. Similarly, install the SQLite.swift package dependency in XCode
|
1. Similarly, install the SQLite.swift package dependency in XCode
|
||||||
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators
|
||||||
|
|
3640
docs/emojis.md
|
@ -16,7 +16,7 @@ I started adding notifications pretty much all of my scripts. Typically, I just
|
||||||
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
|
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
|
||||||
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
|
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
|
||||||
|
|
||||||
``` bash
|
```
|
||||||
rsync -a root@laptop /backups/laptop \
|
rsync -a root@laptop /backups/laptop \
|
||||||
&& zfs snapshot ... \
|
&& zfs snapshot ... \
|
||||||
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
|
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
|
||||||
|
@ -26,7 +26,7 @@ rsync -a root@laptop /backups/laptop \
|
||||||
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
|
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
|
||||||
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
|
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
|
||||||
|
|
||||||
```
|
``` cron
|
||||||
# Check github/ntfy user
|
# Check github/ntfy user
|
||||||
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
|
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
|
||||||
```
|
```
|
||||||
|
@ -135,49 +135,28 @@ You can send a message during a workflow run with curl. Here is an example sendi
|
||||||
${{ secrets.NTFY_URL }}
|
${{ secrets.NTFY_URL }}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Changedetection.io
|
|
||||||
ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
|
|
||||||
[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
|
|
||||||
uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
|
|
||||||
|
|
||||||
To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
|
|
||||||
to the notification list.
|
|
||||||
|
|
||||||
For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
|
|
||||||
|
|
||||||
In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
|
|
||||||
the special ntfy Apprise Notification URL to the Notification List.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Watchtower (shoutrrr)
|
## Watchtower (shoutrrr)
|
||||||
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
|
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
||||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||||
|
|
||||||
Example docker-compose.yml:
|
Example docker-compose.yml:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
services:
|
services:
|
||||||
watchtower:
|
watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
environment:
|
environment:
|
||||||
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
||||||
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
|
- WATCHTOWER_NOTIFICATION_URL=generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you only want to send notifications using shoutrrr:
|
Or, if you only want to send notifications using shoutrrr:
|
||||||
```
|
```
|
||||||
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
||||||
|
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
|
||||||
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->
|
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
|
||||||
|
|
||||||
Radarr, Prowlarr, and Sonarr v4 support ntfy natively under Settings > Connect.
|
|
||||||
|
|
||||||
Sonarr v3, Readarr, and SABnzbd support custom scripts for downloads, warnings, grabs, etc.
|
|
||||||
Some simple bash scripts to achieve this are kindly provided in [nickexyz's ntfy-shellscripts repository](https://github.com/nickexyz/ntfy-shellscripts).
|
|
||||||
|
|
||||||
## Node-RED
|
## Node-RED
|
||||||
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
|
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
|
||||||
|
@ -593,27 +572,4 @@ Example `template.html`:
|
||||||
Add notification on Rundeck (attachment type must be: `Attached as file to email`):
|
Add notification on Rundeck (attachment type must be: `Attached as file to email`):
|
||||||

|

|
||||||
|
|
||||||
## Traccar
|
|
||||||
This will only work on selfhosted [traccar](https://www.traccar.org/) ([Github](https://github.com/traccar/traccar)) instances, as you need to be able to set `sms.http.*` keys, which is not possible through the UI attributes
|
|
||||||
|
|
||||||
The easiest way to integrate traccar with ntfy, is to configure ntfy as the SMS provider for your instance. You then can set your ntfy topic as your account's phone number in traccar. Sending the email notifications to ntfy will not work, as ntfy does not support HTML emails.
|
|
||||||
|
|
||||||
**Caution:** JSON publishing is only possible, when POST-ing to the root URL of the ntfy instance. (see [documentation](publish.md#publish-as-json))
|
|
||||||
```xml
|
|
||||||
<entry key='sms.http.url'>https://ntfy.sh</entry>
|
|
||||||
<entry key='sms.http.template'>
|
|
||||||
{
|
|
||||||
"topic": "{phone}",
|
|
||||||
"message": "{message}"
|
|
||||||
}
|
|
||||||
</entry>
|
|
||||||
```
|
|
||||||
If [access control](config.md#access-control) is enabled, and the target topic does not support anonymous writes, you'll also have to provide an authorization header, for example in form of a privileged token
|
|
||||||
```xml
|
|
||||||
<entry key='sms.http.authorization'>Bearer tk_JhbsnoMrgy2FcfHeofv97Pi5uXaZZ</entry>
|
|
||||||
```
|
|
||||||
or by simply providing traccar with a valid username/password combination.
|
|
||||||
```xml
|
|
||||||
<entry key='sms.http.user'>phil</entry>
|
|
||||||
<entry key='sms.http.password'>mypass</entry>
|
|
||||||
```
|
|
||||||
|
|
28
docs/faq.md
|
@ -43,9 +43,9 @@ of the app and [self-host your own ntfy server](install.md).
|
||||||
## How much battery does the Android app use?
|
## How much battery does the Android app use?
|
||||||
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
|
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
|
||||||
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
|
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
|
||||||
or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
|
or you use *instant delivery* (Android only), the app has to maintain a constant connection to the server, which consumes
|
||||||
the app has to maintain a constant connection to the server, which consumes about 0-1% of battery in 17h of use (on my phone).
|
about 0-1% of battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
|
||||||
There has been a ton of testing and improvement around this. I think it's pretty decent now.
|
decent now.
|
||||||
|
|
||||||
## Paid plans? I thought it was open source?
|
## Paid plans? I thought it was open source?
|
||||||
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
|
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
|
||||||
|
@ -76,29 +76,7 @@ However, if you still want to disable it, you can do so with the `web-root: disa
|
||||||
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
|
||||||
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
|
||||||
|
|
||||||
## If topic names are public, could I not just brute force them?
|
|
||||||
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
|
|
||||||
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
|
|
||||||
the topic is as good as a good password.
|
|
||||||
|
|
||||||
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
|
|
||||||
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
|
|
||||||
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
|
|
||||||
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
|
|
||||||
|
|
||||||
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
|
|
||||||
|
|
||||||
## Where can I donate?
|
## Where can I donate?
|
||||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
|
||||||
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
|
||||||
appreciated.
|
appreciated.
|
||||||
|
|
||||||
## Can I email you? Can I DM you on Discord/Matrix?
|
|
||||||
While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org),
|
|
||||||
[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally
|
|
||||||
**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a
|
|
||||||
[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities.
|
|
||||||
|
|
||||||
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
|
|
||||||
in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users
|
|
||||||
may be able to help out. I hope you understand.
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
def copy_fonts(config, **kwargs):
|
|
||||||
site_dir = config['site_dir']
|
|
||||||
shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
|
|
|
@ -14,53 +14,49 @@ We support amd64, armv7 and arm64.
|
||||||
|
|
||||||
1. Install ntfy using one of the methods described below
|
1. Install ntfy using one of the methods described below
|
||||||
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
||||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||||
|
|
||||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||||
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
|
||||||
for details).
|
for details).
|
||||||
|
|
||||||
If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
|
|
||||||
[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
|
|
||||||
resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
|
|
||||||
|
|
||||||
## Linux binaries
|
## Linux binaries
|
||||||
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
||||||
deb/rpm packages.
|
deb/rpm packages.
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_x86_64.tar.gz
|
||||||
tar zxvf ntfy_2.7.0_linux_amd64.tar.gz
|
tar zxvf ntfy_2.0.0_linux_x86_64.tar.gz
|
||||||
sudo cp -a ntfy_2.7.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.0.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_2.7.0_linux_armv6.tar.gz
|
tar zxvf ntfy_2.0.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_2.7.0_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.0.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_2.7.0_linux_armv7.tar.gz
|
tar zxvf ntfy_2.0.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_2.7.0_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.0.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_2.7.0_linux_arm64.tar.gz
|
tar zxvf ntfy_2.0.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_2.7.0_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.0.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -110,7 +106,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_amd64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
|
@ -118,7 +114,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
|
@ -126,7 +122,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
|
@ -134,7 +130,7 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
|
@ -144,36 +140,34 @@ Manually installing the .deb file:
|
||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_amd64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arch Linux
|
## Arch Linux
|
||||||
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/).
|
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
|
||||||
You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download,
|
|
||||||
build and install ntfy and keep it up to date.
|
|
||||||
```
|
```
|
||||||
paru -S ntfysh-bin
|
paru -S ntfysh-bin
|
||||||
```
|
```
|
||||||
|
@ -195,36 +189,30 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
|
||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
||||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz),
|
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_macOS_all.tar.gz),
|
||||||
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||||
|
|
||||||
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
||||||
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz > ntfy_2.7.0_darwin_all.tar.gz
|
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_macOS_all.tar.gz > ntfy_2.0.0_macOS_all.tar.gz
|
||||||
tar zxvf ntfy_2.7.0_darwin_all.tar.gz
|
tar zxvf ntfy_2.0.0_macOS_all.tar.gz
|
||||||
sudo cp -a ntfy_2.7.0_darwin_all/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.0.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||||
mkdir ~/Library/Application\ Support/ntfy
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
cp ntfy_2.7.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
cp ntfy_2.0.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
ntfy --help
|
ntfy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
Only the ntfy CLI is supported on macOS. ntfy server is currently not supported, but you can build and run it for
|
There is a [GitHub issue](https://github.com/binwiederhier/ntfy/issues/286) about making ntfy installable via
|
||||||
development as well. Check out the [build instructions](develop.md) for details.
|
[Homebrew](https://brew.sh/). I'll eventually get to that, but I'd also love if somebody else stepped up to do it.
|
||||||
|
Also, you can build and run the ntfy server on macOS as well, though I don't officially support that.
|
||||||
## Homebrew
|
Check out the [build instructions](develop.md) for details.
|
||||||
To install the [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) via Homebrew (Linux and macOS),
|
|
||||||
simply run:
|
|
||||||
```
|
|
||||||
brew install ntfy
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
||||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_windows_amd64.zip),
|
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_windows_x86_64.zip),
|
||||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
|
|
||||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||||
|
@ -278,9 +266,9 @@ docker run \
|
||||||
serve
|
serve
|
||||||
```
|
```
|
||||||
|
|
||||||
Using docker-compose with non-root user and healthchecks enabled:
|
Using docker-compose with non-root user:
|
||||||
```yaml
|
```yaml
|
||||||
version: "2.3"
|
version: "2.1"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ntfy:
|
ntfy:
|
||||||
|
@ -296,12 +284,6 @@ services:
|
||||||
- /etc/ntfy:/etc/ntfy
|
- /etc/ntfy:/etc/ntfy
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
healthcheck: # optional: remember to adapt the host:port to your environment
|
|
||||||
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
|
|
||||||
interval: 60s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,22 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been
|
||||||
|
|
||||||
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
|
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
|
||||||
|
|
||||||
|
## Public ntfy servers
|
||||||
|
|
||||||
|
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
|
||||||
|
ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||||
|
|
||||||
|
| URL | Country |
|
||||||
|
|---------------------------------------------------|--------------------|
|
||||||
|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
|
||||||
|
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
|
||||||
|
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
||||||
|
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||||
|
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||||
|
|
||||||
|
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||||
|
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||||
|
|
||||||
## Official integrations
|
## Official integrations
|
||||||
|
|
||||||
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
|
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
|
||||||
|
@ -17,22 +33,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
|
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
|
||||||
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
|
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
|
||||||
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||||
- [Netdata](https://learn.netdata.cloud/docs/alerts-and-notifications/notifications/agent-alert-notifications/ntfy) ⭐ - Real-time performance monitoring
|
|
||||||
- [Deployer](https://github.com/deployphp/deployer) ⭐ - PHP deployment tool
|
|
||||||
- [Scrt.link](https://scrt.link/) - Share a secret
|
- [Scrt.link](https://scrt.link/) - Share a secret
|
||||||
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
|
||||||
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
|
|
||||||
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
|
|
||||||
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
|
|
||||||
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
|
|
||||||
|
|
||||||
## Integration via HTTP/SMTP/etc.
|
|
||||||
|
|
||||||
- [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr))
|
|
||||||
- [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#))
|
|
||||||
- [Overseer](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
|
|
||||||
- [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook))
|
|
||||||
- [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy))
|
|
||||||
|
|
||||||
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
|
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
|
||||||
|
|
||||||
|
@ -56,10 +58,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
|
- [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-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
|
||||||
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
|
||||||
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
|
|
||||||
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
|
|
||||||
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
|
|
||||||
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
|
|
||||||
|
|
||||||
## CLIs + GUIs
|
## CLIs + GUIs
|
||||||
|
|
||||||
|
@ -75,7 +73,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
|
|
||||||
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
|
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
|
||||||
- [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
|
- [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
|
||||||
- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
|
|
||||||
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
|
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
|
||||||
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
|
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
|
||||||
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
|
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
|
||||||
|
@ -85,7 +82,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
|
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
|
||||||
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
|
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
|
||||||
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
|
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
|
||||||
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
|
|
||||||
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
|
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
|
||||||
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
|
||||||
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
|
||||||
|
@ -109,67 +105,15 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
|
||||||
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
|
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
|
||||||
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
|
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
|
||||||
- [woodpecker-ntfy](https://codeberg.org/l-x/woodpecker-ntfy)- Woodpecker CI plugin for sending ntfy notfication from a pipeline (Go)
|
|
||||||
- [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell)
|
- [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)
|
- [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)
|
- [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)
|
- [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
|
- [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
|
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
|
||||||
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
|
|
||||||
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
|
|
||||||
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
|
|
||||||
- [easy2ntfy](https://github.com/chromoxdor/easy2ntfy) - Gateway for ESPeasy to receive commands through ntfy and using easyfetch (HTML/JS)
|
|
||||||
- [ntfy_lite](https://github.com/MPI-IS/ntfy_lite) - Minimalist python API for pushing ntfy notifications (Python)
|
|
||||||
- [notify](https://github.com/guanguans/notify) - 推送通知 (PHP)
|
|
||||||
- [zpool-events](https://github.com/maglar0/zpool-events) - Notify on ZFS pool events (Python)
|
|
||||||
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
|
|
||||||
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
|
|
||||||
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
|
|
||||||
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup and shutdown (Go)
|
|
||||||
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
|
|
||||||
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
|
|
||||||
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
|
|
||||||
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
|
|
||||||
- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
|
|
||||||
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
|
|
||||||
- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
|
|
||||||
- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
|
|
||||||
- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
|
|
||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
|
|
||||||
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
|
|
||||||
- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
|
|
||||||
- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
|
|
||||||
- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
|
|
||||||
- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
|
|
||||||
- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
|
|
||||||
- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
|
|
||||||
- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
|
|
||||||
- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
|
|
||||||
- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
|
|
||||||
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
|
|
||||||
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
|
|
||||||
- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
|
|
||||||
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
|
|
||||||
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
|
|
||||||
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
|
|
||||||
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
|
|
||||||
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
|
|
||||||
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
|
|
||||||
- [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
|
|
||||||
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
|
|
||||||
- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023
|
|
||||||
- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023
|
|
||||||
- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023
|
|
||||||
- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023
|
|
||||||
- [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023
|
|
||||||
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
|
|
||||||
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
|
|
||||||
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
|
|
||||||
- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023
|
|
||||||
- [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023
|
- [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023
|
||||||
- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023
|
- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023
|
||||||
- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023
|
- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023
|
||||||
|
@ -178,13 +122,10 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
|
- [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 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 Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
|
||||||
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
|
|
||||||
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
|
||||||
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
|
||||||
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022
|
|
||||||
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
|
||||||
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
|
||||||
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
|
|
||||||
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.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
|
- [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
|
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022
|
||||||
|
@ -222,23 +163,3 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||||
- [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
|
- [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
|
- [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
|
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021
|
||||||
|
|
||||||
|
|
||||||
## Alternative ntfy servers
|
|
||||||
|
|
||||||
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
|
|
||||||
ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
|
||||||
|
|
||||||
| URL | Country |
|
|
||||||
|---------------------------------------------------|--------------------|
|
|
||||||
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
|
|
||||||
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
|
|
||||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
|
||||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
|
||||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
|
||||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
|
||||||
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
|
|
||||||
| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
|
|
||||||
|
|
||||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
|
||||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# Known issues
|
# Known issues
|
||||||
This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete
|
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
|
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
|
||||||
to have the prominent ones here to link to.
|
to have the prominent ones here to link to.
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ For some (many?) users, the iOS app is not refreshing the view when new notifica
|
||||||
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
|
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
|
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 not clear to me what is causing it.
|
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.
|
Please send experienced iOS developers my way to help me figure this out.
|
||||||
|
|
||||||
|
@ -26,18 +26,3 @@ Be sure that in your selfhosted server:
|
||||||
|
|
||||||
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
|
||||||
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
|
||||||
|
|
||||||
## iOS app seeing "New message", but not real message content
|
|
||||||
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
|
|
||||||
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
|
|
||||||
|
|
||||||
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
|
|
||||||
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
|
|
||||||
|
|
||||||
## Safari does not play sounds for web push notifications
|
|
||||||
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
|
|
||||||
iOS 17 / Safari 17, which will be released later in 2023.
|
|
||||||
|
|
||||||
## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787))
|
|
||||||
When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a
|
|
||||||
[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found.
|
|
||||||
|
|
818
docs/publish.md
329
docs/releases.md
|
@ -2,295 +2,6 @@
|
||||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
## ntfy server v2.7.0
|
|
||||||
Released August 17, 2023
|
|
||||||
|
|
||||||
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
|
|
||||||
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
|
|
||||||
internationalization support, a CLI auth bug.
|
|
||||||
|
|
||||||
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
|
|
||||||
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
|
|
||||||
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
|
|
||||||
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
|
|
||||||
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
|
||||||
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
|
|
||||||
**Security:** ⚠️
|
|
||||||
|
|
||||||
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
|
||||||
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
|
||||||
|
|
||||||
**Documentation:**
|
|
||||||
|
|
||||||
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
|
|
||||||
|
|
||||||
## ntfy server v2.6.2
|
|
||||||
Released June 30, 2023
|
|
||||||
|
|
||||||
With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA)
|
|
||||||
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
|
|
||||||
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
|
|
||||||
push notifications, and an app badge with the unread notification count. Note that for self-hosted servers,
|
|
||||||
[Web Push](config.md#web-push) must be configured.
|
|
||||||
|
|
||||||
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
|
|
||||||
|
|
||||||
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
|
|
||||||
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
|
|
||||||
|
|
||||||
❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
|
||||||
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
|
|
||||||
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
|
|
||||||
**Bug fixes:**
|
|
||||||
|
|
||||||
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
|
|
||||||
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
|
|
||||||
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
|
|
||||||
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
|
|
||||||
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
|
|
||||||
**Maintenance:**
|
|
||||||
|
|
||||||
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
|
|
||||||
|
|
||||||
**Changes in tarball/zip naming:**
|
|
||||||
Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release
|
|
||||||
archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy:
|
|
||||||
|
|
||||||
- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip`
|
|
||||||
- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz`
|
|
||||||
- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz`
|
|
||||||
|
|
||||||
## ntfy server v2.5.0
|
|
||||||
Released May 18, 2023
|
|
||||||
|
|
||||||
This release brings a number of new features, including support for text-to-speech style [phone calls](publish.md#phone-calls),
|
|
||||||
an admin API to manage users and ACL (currently in beta, and hence undocumented), and support for authorized access to
|
|
||||||
upstream servers via the `upstream-access-token` config option.
|
|
||||||
|
|
||||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
|
||||||
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
|
|
||||||
if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Support for text-to-speech style [phone calls](publish.md#phone-calls) using the `X-Call` header (no ticket)
|
|
||||||
* Admin API to manage users and ACL, `v1/users` + `v1/users/access` (intentionally undocumented as of now, [#722](https://github.com/binwiederhier/ntfy/issues/722), thanks to [@CreativeWarlock](https://github.com/CreativeWarlock) for sponsoring this ticket)
|
|
||||||
* Added `upstream-access-token` config option to allow authorized access to upstream servers (no ticket)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Removed old ntfy website from ntfy entirely (no ticket)
|
|
||||||
* Make emoji lookup for emails more efficient ([#725](https://github.com/binwiederhier/ntfy/pull/725), thanks to [@adamantike](https://github.com/adamantike))
|
|
||||||
* Fix potential subscriber ID clash ([#712](https://github.com/binwiederhier/ntfy/issues/712), thanks to [@peterbourgon](https://github.com/peterbourgon) for reporting, and [@dropdevrahul](https://github.com/dropdevrahul) for fixing)
|
|
||||||
* Support for `quoted-printable` in incoming emails ([#719](https://github.com/binwiederhier/ntfy/pull/719), thanks to [@Aerion](https://github.com/Aerion))
|
|
||||||
* Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
|
|
||||||
* Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
|
|
||||||
|
|
||||||
## ntfy server v2.4.0
|
|
||||||
Released Apr 26, 2023
|
|
||||||
|
|
||||||
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
|
|
||||||
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
|
|
||||||
|
|
||||||
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
|
||||||
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
|
|
||||||
will always remain open source.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) can now be installed via Homebrew (thanks to [@Moulick](https://github.com/Moulick))
|
|
||||||
* Added `v1/stats` endpoint to expose messages stats (no ticket)
|
|
||||||
* Support [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2) encoded headers (no ticket, honorable mention to [mqttwarn](https://github.com/jpmens/mqttwarn/pull/638) and [@amotl](https://github.com/amotl))
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Hide country flags on Windows ([#606](https://github.com/binwiederhier/ntfy/issues/606), thanks to [@cmeis](https://github.com/cmeis) for reporting, and to [@pokej6](https://github.com/pokej6) for fixing it)
|
|
||||||
* `ntfy sub` now uses default auth credentials as defined in `client.yml` ([#698](https://github.com/binwiederhier/ntfy/issues/698), thanks to [@CrimsonFez](https://github.com/CrimsonFez) for reporting, and to [@wunter8](https://github.com/wunter8) for fixing it)
|
|
||||||
|
|
||||||
**Documentation:**
|
|
||||||
|
|
||||||
* Updated PowerShell examples ([#697](https://github.com/binwiederhier/ntfy/pull/697), thanks to [@Natfan](https://github.com/Natfan))
|
|
||||||
|
|
||||||
**Additional languages:**
|
|
||||||
|
|
||||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))
|
|
||||||
|
|
||||||
## ntfy server v2.3.1
|
|
||||||
Released March 30, 2023
|
|
||||||
|
|
||||||
This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
|
|
||||||
on ntfy.sh that we observe every 20 minutes. The polling was never strictly necessary, and has actually caused duplicate
|
|
||||||
delivery issues as well, so disabling it should not have any negative effects. iOS users, please reach out via Discord
|
|
||||||
or Matrix if there are issues.
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Disable iOS polling entirely ([#677](https://github.com/binwiederhier/ntfy/issues/677)/[#509](https://github.com/binwiederhier/ntfy/issues/509))
|
|
||||||
|
|
||||||
## ntfy server v2.3.0
|
|
||||||
Released March 29, 2023
|
|
||||||
|
|
||||||
This release primarily fixes an issue with delayed messages, and it adds support for Go's profiler (if enabled), which
|
|
||||||
will allow investigating usage spikes in more detail. There will likely be a follow-up release this week to fix the
|
|
||||||
actual spikes [caused by iOS devices](https://github.com/binwiederhier/ntfy/issues/677).
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* ntfy now supports Go's `pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))
|
|
||||||
* Fixed plural for Polish and other translations ([#678](https://github.com/binwiederhier/ntfy/pull/678), thanks to [@bmoczulski](https://github.com/bmoczulski))
|
|
||||||
|
|
||||||
## ntfy server v2.2.0
|
|
||||||
Released March 17, 2023
|
|
||||||
|
|
||||||
With this release, ntfy is now able to expose metrics via a `/metrics` endpoint for [Prometheus](https://prometheus.io/), if enabled.
|
|
||||||
The endpoint exposes about 20 different counters and gauges, from the number of published messages and emails, to active subscribers,
|
|
||||||
visitors and topics. If you'd like more metrics, pop in the Discord/Matrix or file an issue on GitHub.
|
|
||||||
|
|
||||||
On top of this, you can now use access tokens in the ntfy CLI (defined in the `client.yml` file), fixed a bug in `ntfy subscribe`,
|
|
||||||
removed the dependency on Google Fonts, and more.
|
|
||||||
|
|
||||||
🔥 Reminder: Purchase one of three **ntfy Pro plans** for **50% off** for a limited time (if you use promo code `MYTOPIC`).
|
|
||||||
ntfy Pro gives you higher rate limits and lets you reserve topic names. [Buy through web app](https://ntfy.sh/app).
|
|
||||||
|
|
||||||
❤️ If you don't need ntfy Pro, please consider sponsoring ntfy via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
|
|
||||||
and [Liberapay](https://en.liberapay.com/ntfy/). ntfy will stay open source forever.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Monitoring: ntfy now exposes a `/metrics` endpoint for [Prometheus](https://prometheus.io/) if [configured](config.md#monitoring) ([#210](https://github.com/binwiederhier/ntfy/issues/210), thanks to [@rogeliodh](https://github.com/rogeliodh) for reporting)
|
|
||||||
* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
|
|
||||||
* Docs: Removed dependency on Google Fonts in docs ([#554](https://github.com/binwiederhier/ntfy/issues/554), thanks to [@bt90](https://github.com/bt90) for reporting, and [@ozskywalker](https://github.com/ozskywalker) for implementing)
|
|
||||||
* Increase allowed auth failure attempts per IP address to 30 (no ticket)
|
|
||||||
* Web app: Increase maximum incremental backoff retry interval to 2 minutes (no ticket)
|
|
||||||
|
|
||||||
**Documentation:**
|
|
||||||
|
|
||||||
* Make query parameter description more clear ([#630](https://github.com/binwiederhier/ntfy/issues/630), thanks to [@bbaa-bbaa](https://github.com/bbaa-bbaa) for reporting, and to [@wunter8](https://github.com/wunter8) for a fix)
|
|
||||||
|
|
||||||
## ntfy server v2.1.2
|
|
||||||
Released March 4, 2023
|
|
||||||
|
|
||||||
This is a hotfix release, mostly to combat the ridiculous amount of Matrix requests with invalid/dead pushkeys, and the
|
|
||||||
corresponding HTTP 507 responses the ntfy.sh server is sending out. We're up to >600k HTTP 507 responses per day 🤦. This
|
|
||||||
release solves this issue by rejecting Matrix pushkeys, if nobody has subscribed to the corresponding topic for 12 hours.
|
|
||||||
|
|
||||||
The release furthermore reverts the default rate limiting behavior for UnifiedPush to be publisher-based, and introduces
|
|
||||||
a flag to enable [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) for high volume servers.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Support SMTP servers without auth ([#645](https://github.com/binwiederhier/ntfy/issues/645), thanks to [@Sharknoon](https://github.com/Sharknoon) for reporting)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Token auth doesn't work if default user credentials are defined in `client.yml` ([#650](https://github.com/binwiederhier/ntfy/issues/650), thanks to [@Xinayder](https://github.com/Xinayder))
|
|
||||||
* Add `visitor-subscriber-rate-limiting` flag to allow enabling [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) (off by default now, [#649](https://github.com/binwiederhier/ntfy/issues/649)/[#655](https://github.com/binwiederhier/ntfy/pull/655), thanks to [@barathrm](https://github.com/barathrm) for reporting, and to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
|
|
||||||
* Reject Matrix pushkey after 12 hours of inactivity on a topic, if `visitor-subscriber-rate-limiting` is enabled ([#643](https://github.com/binwiederhier/ntfy/pull/643), thanks to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
|
|
||||||
|
|
||||||
**Additional languages:**
|
|
||||||
|
|
||||||
* Danish (thanks to [@Andersbiha](https://hosted.weblate.org/user/Andersbiha/))
|
|
||||||
|
|
||||||
## ntfy server v2.1.1
|
|
||||||
Released March 1, 2023
|
|
||||||
|
|
||||||
This is a tiny release with a few bug fixes, but it's big for me personally. After almost three months of work,
|
|
||||||
**today I am finally launching the paid plans on ntfy.sh** 🥳 🎉.
|
|
||||||
|
|
||||||
You are now able to purchase one of three plans that'll give you **higher rate limits** (messages, emails, attachment sizes, ...),
|
|
||||||
as well as the ability to **reserve topic names** for your personal use, while at the same time supporting me and the
|
|
||||||
ntfy open source project ❤️. You can check out the pricing, and [purchase plans through the web app](https://ntfy.sh/app) (use
|
|
||||||
promo code `MYTOPIC` for a **50% discount**, limited time only).
|
|
||||||
|
|
||||||
And as I've said many times: Do not worry. **ntfy will always stay open source**, and that includes all features. There
|
|
||||||
are no closed-source features. So if you'd like to run your own server, you can!
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Fix panic when using Firebase without users ([#641](https://github.com/binwiederhier/ntfy/issues/641), thanks to [u/heavybell](https://www.reddit.com/user/heavybell/) for reporting)
|
|
||||||
* Remove health check from `Dockerfile` and [document it](config.md#health-checks) ([#635](https://github.com/binwiederhier/ntfy/issues/635), thanks to [@Andersbiha](https://github.com/Andersbiha))
|
|
||||||
* Upgrade dialog: Disable submit button for free tier (no ticket)
|
|
||||||
* Allow multiple `log-level-overrides` on the same field (no ticket)
|
|
||||||
* Actually remove `ntfy publish --env-topic` flag (as per [deprecations](deprecations.md), no ticket)
|
|
||||||
* Added `billing-contact` config option (no ticket)
|
|
||||||
|
|
||||||
## ntfy server v2.1.0
|
|
||||||
Released February 25, 2023
|
|
||||||
|
|
||||||
This release changes the way UnifiedPush (UP) topics are rate limited from publisher-based rate limiting to subscriber-based
|
|
||||||
rate limiting. This allows UP application servers to send higher volumes, since the subscribers carry the rate limits.
|
|
||||||
However, it also means that UP clients have to subscribe to a topic first before they are allowed to publish. If they do
|
|
||||||
no, clients will receive an HTTP 507 response from the server.
|
|
||||||
|
|
||||||
We also fixed another issue with UnifiedPush: Some Mastodon servers were sending unsupported `Authorization` headers,
|
|
||||||
which ntfy rejected with an HTTP 401. We now ignore unsupported header values.
|
|
||||||
|
|
||||||
As of this release, ntfy also supports sending emails to protected topics, and it ships code to support annual billing
|
|
||||||
cycles (not live yet).
|
|
||||||
|
|
||||||
As part of this release, I also enabled sign-up and login (free accounts only), and I also started reducing the rate
|
|
||||||
limits for anonymous & free users a bit. With the next release and the launch of the paid plan, I'll reduce the limits
|
|
||||||
a bit more. For 90% of users, you should not feel the difference.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* UnifiedPush: Subscriber-based rate limiting for `up*` topics ([#584](https://github.com/binwiederhier/ntfy/pull/584)/[#609](https://github.com/binwiederhier/ntfy/pull/609)/[#633](https://github.com/binwiederhier/ntfy/pull/633), thanks to [@karmanyaahm](https://github.com/karmanyaahm))
|
|
||||||
* Support for publishing to protected topics via email with access tokens ([#612](https://github.com/binwiederhier/ntfy/pull/621), thanks to [@tamcore](https://github.com/tamcore))
|
|
||||||
* Support for base64-encoded and nested multipart emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts))
|
|
||||||
* Payments: Add support for annual billing intervals (no ticket)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Web: Do not disable "Reserve topic" checkbox for admins (no ticket, thanks to @xenrox for reporting)
|
|
||||||
* UnifiedPush: Treat non-Basic/Bearer `Authorization` header like header was not sent ([#629](https://github.com/binwiederhier/ntfy/issues/629), thanks to [@Boebbele](https://github.com/Boebbele) and [@S1m](https://github.com/S1m) for reporting)
|
|
||||||
|
|
||||||
**Documentation:**
|
|
||||||
|
|
||||||
* Added example for [Traccar](https://ntfy.sh/docs/examples/#traccar) ([#631](https://github.com/binwiederhier/ntfy/pull/631), thanks to [tamcore](https://github.com/tamcore))
|
|
||||||
|
|
||||||
**Additional languages:**
|
|
||||||
|
|
||||||
* Arabic (thanks to [@ButterflyOfFire](https://hosted.weblate.org/user/ButterflyOfFire/))
|
|
||||||
|
|
||||||
## ntfy server v2.0.1
|
|
||||||
Released February 17, 2023
|
|
||||||
|
|
||||||
This is a quick bugfix release to address a panic that happens when `attachment-cache-dir` is not set.
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Avoid panic in manager when `attachment-cache-dir` is not set ([#617](https://github.com/binwiederhier/ntfy/issues/617), thanks to [@ksurl](https://github.com/ksurl))
|
|
||||||
* Ensure that calls to standard logger `log.Println` also output JSON (no ticket)
|
|
||||||
|
|
||||||
## ntfy server v2.0.0
|
## ntfy server v2.0.0
|
||||||
Released February 16, 2023
|
Released February 16, 2023
|
||||||
|
|
||||||
|
@ -353,11 +64,6 @@ going. It'll only make ntfy better.
|
||||||
* User account signup, login, topic reservations, access tokens, tiers etc. ([#522](https://github.com/binwiederhier/ntfy/issues/522))
|
* User account signup, login, topic reservations, access tokens, tiers etc. ([#522](https://github.com/binwiederhier/ntfy/issues/522))
|
||||||
* `OPTIONS` method calls are not serviced when the UI is disabled ([#598](https://github.com/binwiederhier/ntfy/issues/598), thanks to [@enticedwanderer](https://github.com/enticedwanderer) for reporting)
|
* `OPTIONS` method calls are not serviced when the UI is disabled ([#598](https://github.com/binwiederhier/ntfy/issues/598), thanks to [@enticedwanderer](https://github.com/enticedwanderer) for reporting)
|
||||||
|
|
||||||
**Special thanks:**
|
|
||||||
|
|
||||||
A big Thank-you goes to everyone who tested the user account and payments work. I very much appreciate all the feedback,
|
|
||||||
suggestions, and bug reports. Thank you, @nwithan8, @deadcade, @xenrox, @cmeis, @wunter8 and the others who I forgot.
|
|
||||||
|
|
||||||
## ntfy server v1.31.0
|
## ntfy server v1.31.0
|
||||||
Released February 14, 2023
|
Released February 14, 2023
|
||||||
|
|
||||||
|
@ -389,6 +95,11 @@ breaking-change upgrade, which required some work to get working again.
|
||||||
|
|
||||||
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
|
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
|
||||||
|
|
||||||
|
**Special thanks:**
|
||||||
|
|
||||||
|
A big Thank-you goes to everyone who tested the user account and payments work. I very much appreciate all the feedback,
|
||||||
|
suggestions, and bug reports. Thank you, @nwithan8, @deadcade, and @xenrox.
|
||||||
|
|
||||||
## ntfy server v1.30.1
|
## ntfy server v1.30.1
|
||||||
Released December 23, 2022 🎅
|
Released December 23, 2022 🎅
|
||||||
|
|
||||||
|
@ -1273,38 +984,10 @@ Released Dec 28, 2021
|
||||||
|
|
||||||
**Features & bug fixes:**
|
**Features & bug fixes:**
|
||||||
|
|
||||||
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
|
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
|
||||||
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
||||||
* Fixing the Santa bug #65
|
* Fixing the Santa bug #65
|
||||||
|
|
||||||
## Older releases
|
## Older releases
|
||||||
For older releases, check out the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
For older releases, check out the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
## Not released yet
|
|
||||||
|
|
||||||
### ntfy server v2.8.0 (UNRELEASED)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
|
|
||||||
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
|
|
||||||
* Add special logic to ignore `Priority` header if it resembled a RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
|
|
||||||
* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
|
|
||||||
* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
|
|
||||||
|
|
||||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
|
||||||
|
|
||||||
**Bug fixes + maintenance:**
|
|
||||||
|
|
||||||
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
|
|
||||||
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
|
||||||
* Bumped all dependencies to the latest versions (no ticket)
|
|
||||||
|
|
||||||
**Additional languages:**
|
|
||||||
|
|
||||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
|
|
||||||
|
|
BIN
docs/static/audio/ntfy-phone-call.mp3
vendored
BIN
docs/static/audio/ntfy-phone-call.ogg
vendored
71
docs/static/css/extra.css
vendored
|
@ -2,15 +2,13 @@
|
||||||
--md-primary-fg-color: #338574;
|
--md-primary-fg-color: #338574;
|
||||||
--md-primary-fg-color--light: #338574;
|
--md-primary-fg-color--light: #338574;
|
||||||
--md-primary-fg-color--dark: #338574;
|
--md-primary-fg-color--dark: #338574;
|
||||||
--md-footer-bg-color: #353744;
|
|
||||||
--md-text-font: "Roboto";
|
|
||||||
--md-code-font: "Roboto Mono";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-header__button.md-logo :is(img, svg) {
|
.md-header__button.md-logo :is(img, svg) {
|
||||||
width: unset !important;
|
width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.md-header__topic:first-child {
|
.md-header__topic:first-child {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
@ -71,18 +69,7 @@ figure video {
|
||||||
}
|
}
|
||||||
|
|
||||||
.remove-md-box td {
|
.remove-md-box td {
|
||||||
padding: 0 10px;
|
padding: 0 10px
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-table .c {
|
|
||||||
vertical-align: middle !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-table .e {
|
|
||||||
font-size: 2.5em;
|
|
||||||
padding: 0 2px !important;
|
|
||||||
text-align: center !important;
|
|
||||||
vertical-align: middle !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
|
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
|
||||||
|
@ -160,57 +147,3 @@ figure video {
|
||||||
.lightbox .close-lightbox:hover::before {
|
.lightbox .close-lightbox:hover::before {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* roboto-300 - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
src: url('../fonts/roboto-v30-latin-300.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* roboto-regular - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* roboto-italic - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/roboto-v30-latin-italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* roboto-500 - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
src: url('../fonts/roboto-v30-latin-500.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* roboto-700 - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../fonts/roboto-v30-latin-700.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* roboto-mono - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap;
|
|
||||||
font-family: 'Roboto Mono';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/roboto-mono-v22-latin-regular.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
BIN
docs/static/fonts/roboto-v30-latin-300.woff2
vendored
BIN
docs/static/fonts/roboto-v30-latin-500.woff2
vendored
BIN
docs/static/fonts/roboto-v30-latin-700.woff2
vendored
BIN
docs/static/fonts/roboto-v30-latin-italic.woff2
vendored
BIN
docs/static/fonts/roboto-v30-latin-regular.woff2
vendored
BIN
docs/static/img/android-screenshot-logs.jpg
vendored
Before Width: | Height: | Size: 35 KiB |
BIN
docs/static/img/cdio-setup.jpg
vendored
Before Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/favicon.ico
vendored
Before Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/favicon.png
vendored
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
docs/static/img/grafana-dashboard.png
vendored
Before Width: | Height: | Size: 334 KiB |
BIN
docs/static/img/pwa-badge.png
vendored
Before Width: | Height: | Size: 185 KiB |
BIN
docs/static/img/pwa-install-chrome-android-menu.jpg
vendored
Before Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/pwa-install-chrome-android-popup.jpg
vendored
Before Width: | Height: | Size: 69 KiB |
BIN
docs/static/img/pwa-install-chrome-android.jpg
vendored
Before Width: | Height: | Size: 112 KiB |
BIN
docs/static/img/pwa-install-firefox-android-menu.jpg
vendored
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 162 KiB |
BIN
docs/static/img/pwa-install-safari-ios-add-icon.jpg
vendored
Before Width: | Height: | Size: 100 KiB |
BIN
docs/static/img/pwa-install-safari-ios-button.jpg
vendored
Before Width: | Height: | Size: 103 KiB |
BIN
docs/static/img/pwa-install-safari-ios-menu.jpg
vendored
Before Width: | Height: | Size: 124 KiB |
BIN
docs/static/img/pwa-install.png
vendored
Before Width: | Height: | Size: 107 KiB |
BIN
docs/static/img/pwa.png
vendored
Before Width: | Height: | Size: 92 KiB |
BIN
docs/static/img/web-logs.png
vendored
Before Width: | Height: | Size: 72 KiB |
BIN
docs/static/img/web-markdown.png
vendored
Before Width: | Height: | Size: 248 KiB |
BIN
docs/static/img/web-phone-verify.png
vendored
Before Width: | Height: | Size: 22 KiB |
BIN
docs/static/img/web-pin.png
vendored
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
docs/static/img/web-subscribe.png
vendored
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 76 KiB |
|
@ -319,7 +319,7 @@ format of the message. It's very straight forward:
|
||||||
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
|
| `id` | ✔️ | *string* | `hwQ2YpKdmg` | Randomly chosen message identifier |
|
||||||
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
|
| `time` | ✔️ | *number* | `1635528741` | Message date time, as Unix time stamp |
|
||||||
| `expires` | (✔)️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted, not set if `Cache: no` is sent |
|
| `expires` | ✔️ | *number* | `1673542291` | Unix time stamp indicating when the message will be deleted |
|
||||||
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
|
| `event` | ✔️ | `open`, `keepalive`, `message`, or `poll_request` | `message` | Message type, typically you'd be only interested in `message` |
|
||||||
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
|
| `topic` | ✔️ | *string* | `topic1,topic2` | Comma-separated list of topics the message is associated with; only one for all `message` events, but may be a list in `open` events |
|
||||||
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
|
| `message` | - | *string* | `Some message` | Message body; always present in `message` events |
|
||||||
|
|
|
@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c
|
||||||
## Install + configure
|
## Install + configure
|
||||||
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
|
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
|
||||||
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
|
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
|
||||||
by creating `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user). You
|
by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You
|
||||||
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
|
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
|
||||||
|
|
||||||
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
|
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
|
||||||
|
@ -254,13 +254,13 @@ I hope this shows how powerful this command is. Here's a short video that demons
|
||||||
<figcaption>Execute all the things</figcaption>
|
<figcaption>Execute all the things</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
If most (or all) of your subscriptions use the same credentials, you can set defaults in `client.yml`. Use `default-user` and `default-password` or `default-token` (but not both).
|
If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
|
||||||
You can also specify a `default-command` that will run when a message is received. If a subscription does not include credentials to use or does not have a command, the defaults
|
`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
|
||||||
will be used, otherwise, the subscription settings will override the defaults.
|
override the defaults.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Because the `default-user`, `default-password`, and `default-token` will be sent for each topic that does not have its own username/password (even if the topic does not
|
Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
|
||||||
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||||
|
|
||||||
### Using the systemd service
|
### Using the systemd service
|
||||||
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||||
|
|
|
@ -12,9 +12,6 @@ You can get the Android app from both [Google Play](https://play.google.com/stor
|
||||||
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
|
from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that
|
||||||
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||||
|
|
||||||
Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app.
|
|
||||||
The PWA is a website that you can add to your home screen, and it will behave just like a native app.
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
|
A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty
|
||||||
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.
|
straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them.
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
# Using the progressive web app (PWA)
|
|
||||||
While ntfy doesn't have a native desktop app, it is built as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA)
|
|
||||||
and thus can be **installed on both desktop and mobile devices**.
|
|
||||||
|
|
||||||
This gives it its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux, home screen icon on iOS, and
|
|
||||||
launcher icon on Android), a standalone window, push notifications, and an app badge with the unread notification count.
|
|
||||||
|
|
||||||
Web app installation is **supported on** (see [compatibility table](https://caniuse.com/web-app-manifest) for details):
|
|
||||||
|
|
||||||
- **Chrome:** Android, Windows, Linux, macOS
|
|
||||||
- **Safari:** iOS 16.4+, macOS 14+
|
|
||||||
- **Firefox:** Android, as well as on Windows/Linux [via an extension](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/)
|
|
||||||
- **Edge:** Windows
|
|
||||||
|
|
||||||
Note that for self-hosted servers, [Web Push](../config.md#web-push) must be configured for the PWA to work.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Chrome on Desktop
|
|
||||||
To install and register the web app via Chrome, click the "install app" icon. After installation, you can find the app in your
|
|
||||||
app drawer:
|
|
||||||
|
|
||||||
<div id="pwa-screenshots-chrome-safari-desktop" class="screenshots">
|
|
||||||
<a href="../../static/img/pwa-install.png"><img src="../../static/img/pwa-install.png"/></a>
|
|
||||||
<a href="../../static/img/pwa.png"><img src="../../static/img/pwa.png"/></a>
|
|
||||||
<a href="../../static/img/pwa-badge.png"><img src="../../static/img/pwa-badge.png"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Safari on macOS
|
|
||||||
To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.
|
|
||||||
|
|
||||||
<div id="pwa-screenshots-safari-desktop" class="screenshots">
|
|
||||||
<a href="../../static/img/pwa-install-macos-safari-add-to-dock.png"><img src="../../static/img/pwa-install-macos-safari-add-to-dock.png"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Chrome/Firefox on Android
|
|
||||||
For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
|
|
||||||
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
|
|
||||||
and on your home screen.
|
|
||||||
|
|
||||||
<div id="pwa-screenshots-chrome-android" class="screenshots">
|
|
||||||
<a href="../../static/img/pwa-install-chrome-android.jpg"><img src="../../static/img/pwa-install-chrome-android.jpg"/></a>
|
|
||||||
<a href="../../static/img/pwa-install-chrome-android-menu.jpg"><img src="../../static/img/pwa-install-chrome-android-menu.jpg"/></a>
|
|
||||||
<a href="../../static/img/pwa-install-chrome-android-popup.jpg"><img src="../../static/img/pwa-install-chrome-android-popup.jpg"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
For Firefox, select "Install" in the menu, and then click "Add" to add an icon to your home screen:
|
|
||||||
|
|
||||||
<div id="pwa-screenshots-firefox-android" class="screenshots">
|
|
||||||
<a href="../../static/img/pwa-install-firefox-android-menu.jpg"><img src="../../static/img/pwa-install-firefox-android-menu.jpg"/></a>
|
|
||||||
<a href="../../static/img/pwa-install-firefox-android-popup.jpg"><img src="../../static/img/pwa-install-firefox-android-popup.jpg"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Safari on iOS
|
|
||||||
On iOS Safari, tap on the Share menu, then tap "Add to Home Screen":
|
|
||||||
|
|
||||||
<div id="pwa-screenshots-safari-ios" class="screenshots">
|
|
||||||
<a href="../../static/img/pwa-install-safari-ios-button.jpg"><img src="../../static/img/pwa-install-safari-ios-button.jpg"/></a>
|
|
||||||
<a href="../../static/img/pwa-install-safari-ios-menu.jpg"><img src="../../static/img/pwa-install-safari-ios-menu.jpg"/></a>
|
|
||||||
<a href="../../static/img/pwa-install-safari-ios-add-icon.jpg"><img src="../../static/img/pwa-install-safari-ios-add-icon.jpg"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Background notifications
|
|
||||||
Background notifications via web push are enabled by default and cannot be turned off when the app is installed, as notifications would
|
|
||||||
not be delivered reliably otherwise. You can mute topics you don't want to receive notifications for.
|
|
||||||
|
|
||||||
On desktop, you generally need either your browser or the web app open to receive notifications, though the ntfy tab doesn't need to be
|
|
||||||
open. On mobile, you don't need to have the web app open to receive notifications. Look at the [web docs](./web.md#background-notifications)
|
|
||||||
for a detailed breakdown.
|
|
|
@ -1,75 +1,27 @@
|
||||||
# Subscribe from the web app
|
# Subscribe from the Web UI
|
||||||
The web app lets you subscribe and publish messages to ntfy topics. For ntfy.sh, the web app is available at [ntfy.sh/app](https://ntfy.sh/app).
|
You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, **notifications will
|
||||||
To subscribe, simply type in the topic name and click the *Subscribe* button. **After subscribing, messages published to the topic
|
pop up as desktop notifications**. Simply type in the topic name and click the *Subscribe* button. The browser will
|
||||||
will appear in the web app, and pop up as a notification.**
|
keep a connection open and listen for incoming notifications.
|
||||||
|
|
||||||
<div id="subscribe-screenshots" class="screenshots">
|
|
||||||
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Publish messages
|
|
||||||
To learn how to send messages, check out the [publishing page](../publish.md).
|
To learn how to send messages, check out the [publishing page](../publish.md).
|
||||||
|
|
||||||
<div id="web-screenshots" class="screenshots">
|
<div id="web-screenshots" class="screenshots">
|
||||||
<a href="../../static/img/web-detail.png"><img src="../../static/img/web-detail.png"/></a>
|
<a href="../../static/img/web-detail.png"><img src="../../static/img/web-detail.png"/></a>
|
||||||
<a href="../../static/img/web-notification.png"><img src="../../static/img/web-notification.png"/></a>
|
<a href="../../static/img/web-notification.png"><img src="../../static/img/web-notification.png"/></a>
|
||||||
|
<a href="../../static/img/web-subscribe.png"><img src="../../static/img/web-subscribe.png"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Topic reservations
|
To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend,
|
||||||
|
is to pin the tab so that it's always open, but sort of out of the way:
|
||||||
|
|
||||||
|
<figure markdown>
|
||||||
|
{ width=500 }
|
||||||
|
<figcaption>Pin web app to move it out of the way</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
If topic reservations are enabled, you can claim ownership over topics and define access to it:
|
If topic reservations are enabled, you can claim ownership over topics and define access to it:
|
||||||
|
|
||||||
<div id="reserve-screenshots" class="screenshots">
|
<div id="reserve-screenshots" class="screenshots">
|
||||||
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
|
<a href="../../static/img/web-reserve-topic.png"><img src="../../static/img/web-reserve-topic.png"/></a>
|
||||||
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
|
<a href="../../static/img/web-reserve-topic-dialog.png"><img src="../../static/img/web-reserve-topic-dialog.png"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Notification features and browser support
|
|
||||||
|
|
||||||
- Emoji tags are supported in all browsers
|
|
||||||
|
|
||||||
- [Click](../publish.md#click-action) actions are supported in all browsers
|
|
||||||
|
|
||||||
- Only Chrome, Edge, and Opera support displaying view and http [actions](../publish.md#action-buttons) in notifications.
|
|
||||||
|
|
||||||
Their presentation is platform specific.
|
|
||||||
|
|
||||||
Note that HTTP actions are performed using fetch and thus are limited to the [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
||||||
rules, which means that any URL you include needs to respond to a [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)
|
|
||||||
with headers allowing the origin of the ntfy web app (`Access-Control-Allow-Origin: https://ntfy.sh`) or `*`.
|
|
||||||
|
|
||||||
- Only Chrome, Edge, and Opera support displaying [images](../publish.md#attachments) in notifications.
|
|
||||||
|
|
||||||
Look at the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility)
|
|
||||||
for more info.
|
|
||||||
|
|
||||||
## Background notifications
|
|
||||||
While subscribing, you have the option to enable background notifications on supported browsers (see "Settings" tab).
|
|
||||||
|
|
||||||
Note: If you add the web app to your homescreen (as a progressive web app, more info in the [installed web app](pwa.md)
|
|
||||||
docs), you cannot turn these off, as notifications would not be delivered reliably otherwise. You can mute topics you don't want to receive
|
|
||||||
notifications for.
|
|
||||||
|
|
||||||
**If background notifications are off:** This requires an active ntfy tab to be open to receive notifications.
|
|
||||||
These are typically instantaneous, and will appear as a system notification. If you don't see these, check that your browser
|
|
||||||
is allowed to show notifications (for example in System Settings on macOS). If you don't want to enable background notifications,
|
|
||||||
**pinning the ntfy tab on your browser** is a good solution to leave it running.
|
|
||||||
|
|
||||||
**If background notifications are on:** This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active
|
|
||||||
ntfy tab open, but in some cases you may need to keep your browser open. Background notifications are only supported on the
|
|
||||||
same server hosting the web app. You cannot use another server, but can instead subscribe on the other server itself.
|
|
||||||
|
|
||||||
If the ntfy app is not opened for more than a week, background notifications will be paused. You can resume them
|
|
||||||
by opening the app again, and will get a warning notification before they are paused.
|
|
||||||
|
|
||||||
| Browser | Platform | Browser Running | Browser Not Running | Restrictions |
|
|
||||||
|---------|----------|-----------------|---------------------|---------------------------------------------------------|
|
|
||||||
| Chrome | Desktop | ✅ | ❌ | |
|
|
||||||
| Firefox | Desktop | ✅ | ❌ | |
|
|
||||||
| Edge | Desktop | ✅ | ❌ | |
|
|
||||||
| Opera | Desktop | ✅ | ❌ | |
|
|
||||||
| Safari | Desktop | ✅ | ✅ | requires Safari 16.1, macOS 13 Ventura |
|
|
||||||
| Chrome | Android | ✅ | ✅ | |
|
|
||||||
| Firefox | Android | ✅ | ✅ | |
|
|
||||||
| Safari | iOS | ⚠️ | ⚠️ | requires iOS 16.4, only when app is added to homescreen |
|
|
||||||
|
|
||||||
(Browsers below 1% usage not shown, look at the [Push API](https://caniuse.com/push-api) for more info)
|
|
||||||
|
|
|
@ -1,131 +0,0 @@
|
||||||
# Troubleshooting
|
|
||||||
This page lists a few suggestions of what to do when things don't work as expected. This is not a complete list.
|
|
||||||
If this page does not help, feel free to drop by the [Discord](https://discord.gg/cT7ECsZj9w) or [Matrix](https://matrix.to/#/#ntfy:matrix.org)
|
|
||||||
and ask there. We're happy to help.
|
|
||||||
|
|
||||||
## ntfy server
|
|
||||||
If you host your own ntfy server, and you're having issues with any component, it is always helpful to enable debugging/tracing
|
|
||||||
in the server. You can find detailed instructions in the [Logging & Debugging](config.md#logging-debugging) section, but it ultimately
|
|
||||||
boils down to setting `log-level: debug` or `log-level: trace` in the `server.yml` file:
|
|
||||||
|
|
||||||
=== "server.yml (debug)"
|
|
||||||
``` yaml
|
|
||||||
log-level: debug
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "server.yml (trace)"
|
|
||||||
``` yaml
|
|
||||||
log-level: trace
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're using environment variables, set `NTFY_LOG_LEVEL=debug` (or `trace`) instead. You can also pass `--debug` or `--trace`
|
|
||||||
to the `ntfy serve` command, e.g. `ntfy serve --trace`. If you're using systemd (i.e. `systemctl`) to run ntfy, you can look at
|
|
||||||
the logs using `journalctl -u ntfy -f`. The logs will look something like this:
|
|
||||||
|
|
||||||
=== "Example logs (debug)"
|
|
||||||
```
|
|
||||||
$ ntfy serve --debug
|
|
||||||
2023/03/20 14:45:38 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is DEBUG (tag=startup)
|
|
||||||
2023/03/20 14:45:38 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
|
|
||||||
2023/03/20 14:45:39 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
|
||||||
2023/03/20 14:45:39 DEBUG HTTP request started (http_method=POST, http_path=/mytopic, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
|
||||||
2023/03/20 14:45:39 DEBUG Received message (http_method=POST, http_path=/mytopic, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:45:38.319-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002132248, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
|
||||||
2023/03/20 14:45:39 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=EZu6i2WZjH0v, message_sender=127.0.0.1, message_time=1679337939, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000259165, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
|
||||||
2023/03/20 14:45:39 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=2, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0004147334, visitor_seen=2023-03-20T14:45:39.7-04:00)
|
|
||||||
2023/03/20 14:45:39 DEBUG Wrote 1 message(s) in 8.285712ms (tag=message_cache)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
=== "Example logs (trace)"
|
|
||||||
```
|
|
||||||
$ ntfy serve --trace
|
|
||||||
2023/03/20 14:40:42 INFO Listening on :2586[http] :1025[smtp], ntfy 2.1.2, log level is TRACE (tag=startup)
|
|
||||||
2023/03/20 14:40:42 DEBUG Waiting until 2023-03-21 00:00:00 +0000 UTC to reset visitor stats (tag=resetter)
|
|
||||||
2023/03/20 14:40:59 DEBUG Rate limiters reset for visitor (visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:40:59 TRACE HTTP request started (http_method=POST, http_path=/mytopic, http_request=POST /mytopic HTTP/1.1
|
|
||||||
User-Agent: curl/7.81.0
|
|
||||||
Accept: */*
|
|
||||||
Content-Length: 2
|
|
||||||
Content-Type: application/x-www-form-urlencoded
|
|
||||||
|
|
||||||
hi, tag=http, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=0, visitor_messages_limit=500, visitor_messages_remaining=500, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=60, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:40:59 TRACE Received message (http_method=POST, http_path=/mytopic, message_body={
|
|
||||||
"id": "Khaup1RVclU3",
|
|
||||||
"time": 1679337659,
|
|
||||||
"expires": 1679380859,
|
|
||||||
"event": "message",
|
|
||||||
"topic": "mytopic",
|
|
||||||
"message": "hi"
|
|
||||||
}, message_body_size=2, message_delayed=false, message_email=, message_event=message, message_firebase=true, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, message_unifiedpush=false, tag=publish, topic=mytopic, topic_last_access=2023-03-20T14:40:59.893-04:00, topic_subscribers=0, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0001785048, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:40:59 DEBUG Adding message to cache (http_method=POST, http_path=/mytopic, message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002044368, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:40:59 DEBUG HTTP request finished (http_method=POST, http_path=/mytopic, tag=http, time_taken_ms=1, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.000220502, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:40:59 TRACE No stream or WebSocket subscribers, not forwarding (message_body_size=2, message_event=message, message_id=Khaup1RVclU3, message_sender=127.0.0.1, message_time=1679337659, tag=publish, topic=mytopic, visitor_auth_limiter_limit=0.016666666666666666, visitor_auth_limiter_tokens=10, visitor_emails=0, visitor_emails_limit=12, visitor_emails_remaining=12, visitor_id=ip:127.0.0.1, visitor_ip=127.0.0.1, visitor_messages=1, visitor_messages_limit=500, visitor_messages_remaining=499, visitor_request_limiter_limit=0.2, visitor_request_limiter_tokens=59.0002369212, visitor_seen=2023-03-20T14:40:59.893-04:00)
|
|
||||||
2023/03/20 14:41:00 DEBUG Wrote 1 message(s) in 9.529196ms (tag=message_cache)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Android app
|
|
||||||
On Android, you can turn on logging in the settings under **Settings → Record logs**. This will store up to 1,000 log
|
|
||||||
entries, which you can then copy or upload.
|
|
||||||
|
|
||||||
<figure markdown>
|
|
||||||
{ width=400 }
|
|
||||||
<figcaption>Recording logs on Android</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
When you copy or upload the logs, you can censor them to make it easier to share them with others. ntfy will replace all
|
|
||||||
topics and hostnames with fruits. Here's an example:
|
|
||||||
|
|
||||||
```
|
|
||||||
This is a log of the ntfy Android app. The log shows up to 1,000 entries.
|
|
||||||
Server URLs (aside from ntfy.sh) and topics have been replaced with fruits 🍌🥝🍋🥥🥑🍊🍎🍑.
|
|
||||||
|
|
||||||
Device info:
|
|
||||||
--
|
|
||||||
ntfy: 1.16.0 (play)
|
|
||||||
OS: 4.19.157-perf+
|
|
||||||
Android: 13 (SDK 33)
|
|
||||||
...
|
|
||||||
|
|
||||||
Logs
|
|
||||||
--
|
|
||||||
|
|
||||||
1679339199507 2023-03-20 15:06:39.507 D NtfyMainActivity Battery: ignoring optimizations = true (we want this to be true); instant subscriptions = true; remind time reached = true; banner = false
|
|
||||||
1679339199507 2023-03-20 15:06:39.507 D NtfySubscriberMgr Enqueuing work to refresh subscriber service
|
|
||||||
1679339199589 2023-03-20 15:06:39.589 D NtfySubscriberMgr ServiceStartWorker: Starting foreground service with action START (work ID: a7eeeae9-9356-40df-afbd-236e5ed10a0b)
|
|
||||||
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService onStartCommand executed with startId: 262
|
|
||||||
1679339199602 2023-03-20 15:06:39.602 D NtfySubscriberService using an intent with action START
|
|
||||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService Refreshing subscriptions
|
|
||||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Desired connections: [ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869}), ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328})]
|
|
||||||
1679339199629 2023-03-20 15:06:39.629 D NtfySubscriberService - Active connections: [ConnectionId(baseUrl=https://orange.example.com, topicsToSubscriptionIds={apple=4971265, dragonfruit=66809328}), ConnectionId(baseUrl=https://ntfy.sh, topicsToSubscriptionIds={avocado=23801492, lemon=49013182, banana=1309176509201171073, peach=573300885184666424, pineapple=-5956897229801209316, durian=81453333, starfruit=30489279, fruit12=82532869})]
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
To get live logs, or to get more advanced access to an Android phone, you can use [adb](https://developer.android.com/studio/command-line/adb).
|
|
||||||
After you install and [enable adb debugging](https://developer.android.com/studio/command-line/adb#Enabling), you can
|
|
||||||
get detailed logs like so:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Connect to phone (enable Wireless debugging first)
|
|
||||||
adb connect 192.168.1.137:39539
|
|
||||||
|
|
||||||
# Print all logs; you may have to pass the -s option
|
|
||||||
adb logcat
|
|
||||||
adb -s 192.168.1.137:39539 logcat
|
|
||||||
|
|
||||||
# Only list ntfy logs
|
|
||||||
adb logcat --pid=$(adb shell pidof -s io.heckel.ntfy)
|
|
||||||
adb -s 192.168.1.137:39539 logcat --pid=$(adb -s 192.168.1.137:39539 shell pidof -s io.heckel.ntfy)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Web app
|
|
||||||
The web app logs everything to the **developer console**, which you can open by **pressing the F12 key** on your
|
|
||||||
keyboard.
|
|
||||||
|
|
||||||
<figure markdown>
|
|
||||||

|
|
||||||
<figcaption>Web app logs in the developer console</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
## iOS app
|
|
||||||
Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode.
|
|
90
go.mod
|
@ -1,79 +1,65 @@
|
||||||
module git.zio.sh/astra/ntfy/v2
|
module heckel.io/ntfy
|
||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.14.0 // indirect
|
cloud.google.com/go/firestore v1.9.0 // indirect
|
||||||
cloud.google.com/go/storage v1.34.1 // indirect
|
cloud.google.com/go/storage v1.29.0 // indirect
|
||||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/emersion/go-smtp v0.18.0
|
github.com/emersion/go-smtp v0.16.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3
|
github.com/gabriel-vasile/mimetype v1.4.1
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.18
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/olebedev/when v1.0.0
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/urfave/cli/v2 v2.25.7
|
github.com/urfave/cli/v2 v2.24.4
|
||||||
golang.org/x/crypto v0.14.0
|
golang.org/x/crypto v0.6.0
|
||||||
golang.org/x/oauth2 v0.13.0 // indirect
|
golang.org/x/oauth2 v0.5.0 // indirect
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/term v0.13.0
|
golang.org/x/term v0.5.0
|
||||||
golang.org/x/time v0.4.0
|
golang.org/x/time v0.3.0
|
||||||
google.golang.org/api v0.149.0
|
google.golang.org/api v0.110.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839
|
|
||||||
|
|
||||||
require github.com/pkg/errors v0.9.1 // indirect
|
require github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
firebase.google.com/go/v4 v4.12.1
|
firebase.google.com/go/v4 v4.10.0
|
||||||
github.com/SherClockHolmes/webpush-go v1.3.0
|
github.com/stripe/stripe-go/v74 v74.7.0
|
||||||
github.com/prometheus/client_golang v1.17.0
|
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.110.10 // indirect
|
cloud.google.com/go v0.110.0 // indirect
|
||||||
cloud.google.com/go/compute v1.23.3 // indirect
|
cloud.google.com/go/compute v1.18.0 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
cloud.google.com/go/iam v1.1.5 // indirect
|
cloud.google.com/go/iam v0.10.0 // indirect
|
||||||
cloud.google.com/go/longrunning v0.5.4 // indirect
|
cloud.google.com/go/longrunning v0.4.1 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/google/uuid v1.4.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
|
||||||
github.com/prometheus/common v0.45.0 // indirect
|
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
golang.org/x/net v0.17.0 // indirect
|
golang.org/x/net v0.7.0 // indirect
|
||||||
golang.org/x/sys v0.14.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/appengine/v2 v2.0.5 // indirect
|
google.golang.org/appengine/v2 v2.0.2 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 // indirect
|
google.golang.org/grpc v1.53.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
google.golang.org/grpc v1.59.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
206
go.sum
|
@ -1,64 +1,56 @@
|
||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||||
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
|
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY=
|
||||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
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 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw=
|
cloud.google.com/go/firestore v1.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA=
|
||||||
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
|
cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE=
|
||||||
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI=
|
||||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
|
||||||
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
|
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||||
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
|
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
|
||||||
cloud.google.com/go/storage v1.34.1 h1:H2Af2dU5J0PF7A5B+ECFIce+RqxVnrVilO+cu0TS3MI=
|
cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI=
|
||||||
cloud.google.com/go/storage v1.34.1/go.mod h1:VN1ElqqvR9adg1k9xlkUJ55cMOP1/QjnNNuT5xQL6dY=
|
cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4=
|
||||||
firebase.google.com/go/v4 v4.12.1 h1:tDNvobifGsx/1HSFLnM0fmNfx/CDZSgsTO2KhZtgpcs=
|
firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4=
|
||||||
firebase.google.com/go/v4 v4.12.1/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
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 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
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 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||||
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
|
|
||||||
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
|
github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||||
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
@ -68,9 +60,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
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.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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
@ -78,42 +69,27 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
||||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
|
||||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
|
||||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
|
||||||
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
|
|
||||||
github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
|
||||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
|
||||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
|
||||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
@ -125,117 +101,90 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
github.com/stripe/stripe-go/v74 v74.7.0 h1:KHlyslQj9YOv62b1sycQ31LFj7KlqR+seHsSowAWrjc=
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
github.com/stripe/stripe-go/v74 v74.7.0/go.mod h1:5PoXNp30AJ3tGq57ZcFuaMylzNi8KpwlrYAFmO1fHZw=
|
||||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
github.com/urfave/cli/v2 v2.24.4 h1:0gyJJEBYtCV87zI/x2nZCPyDxD51K6xM8SkwjHFCNEU=
|
||||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.24.4/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|
||||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
|
||||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
|
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
|
||||||
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
|
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
|
||||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
|
||||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
|
||||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|
||||||
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
|
||||||
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY=
|
google.golang.org/api v0.110.0 h1:l+rh0KYUooe9JGbGVx71tbFo4SMbMTXK3I3ia2QSEeU=
|
||||||
google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
|
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
|
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
|
||||||
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U=
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc h1:ijGwO+0vL2hJt5gaygqP2j6PfflOBrRot0IczKbmtio=
|
||||||
google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4=
|
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 h1:HJMDndgxest5n2y77fnErkM62iUsptE/H8p0dC2Huo4=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
@ -247,11 +196,10 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
google.golang.org/protobuf v1.31.0/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
92
log/event.go
|
@ -3,7 +3,6 @@ package log
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.zio.sh/astra/ntfy/v2/util"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -12,11 +11,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fieldTag = "tag"
|
tagField = "tag"
|
||||||
fieldError = "error"
|
errorField = "error"
|
||||||
fieldTimeTaken = "time_taken_ms"
|
timeTakenField = "time_taken_ms"
|
||||||
fieldExitCode = "exit_code"
|
exitCodeField = "exit_code"
|
||||||
tagStdLog = "stdlog"
|
timestampFormat = "2006-01-02T15:04:05.999Z07:00"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents a single log event
|
// Event represents a single log event
|
||||||
|
@ -41,39 +40,39 @@ func newEvent() *Event {
|
||||||
|
|
||||||
// Fatal logs the event as FATAL, and exits the program with exit code 1
|
// Fatal logs the event as FATAL, and exits the program with exit code 1
|
||||||
func (e *Event) Fatal(message string, v ...any) {
|
func (e *Event) Fatal(message string, v ...any) {
|
||||||
e.Field(fieldExitCode, 1).Log(FatalLevel, message, v...)
|
e.Field(exitCodeField, 1).maybeLog(FatalLevel, message, v...)
|
||||||
fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr
|
fmt.Fprintf(os.Stderr, message+"\n", v...) // Always output error to stderr
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs the event with log level error
|
// Error logs the event with log level error
|
||||||
func (e *Event) Error(message string, v ...any) *Event {
|
func (e *Event) Error(message string, v ...any) {
|
||||||
return e.Log(ErrorLevel, message, v...)
|
e.maybeLog(ErrorLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs the event with log level warn
|
// Warn logs the event with log level warn
|
||||||
func (e *Event) Warn(message string, v ...any) *Event {
|
func (e *Event) Warn(message string, v ...any) {
|
||||||
return e.Log(WarnLevel, message, v...)
|
e.maybeLog(WarnLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs the event with log level info
|
// Info logs the event with log level info
|
||||||
func (e *Event) Info(message string, v ...any) *Event {
|
func (e *Event) Info(message string, v ...any) {
|
||||||
return e.Log(InfoLevel, message, v...)
|
e.maybeLog(InfoLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs the event with log level debug
|
// Debug logs the event with log level debug
|
||||||
func (e *Event) Debug(message string, v ...any) *Event {
|
func (e *Event) Debug(message string, v ...any) {
|
||||||
return e.Log(DebugLevel, message, v...)
|
e.maybeLog(DebugLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace logs the event with log level trace
|
// Trace logs the event with log level trace
|
||||||
func (e *Event) Trace(message string, v ...any) *Event {
|
func (e *Event) Trace(message string, v ...any) {
|
||||||
return e.Log(TraceLevel, message, v...)
|
e.maybeLog(TraceLevel, message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag adds a "tag" field to the log event
|
// Tag adds a "tag" field to the log event
|
||||||
func (e *Event) Tag(tag string) *Event {
|
func (e *Event) Tag(tag string) *Event {
|
||||||
return e.Field(fieldTag, tag)
|
return e.Field(tagField, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time sets the time field
|
// Time sets the time field
|
||||||
|
@ -86,7 +85,7 @@ func (e *Event) Time(t time.Time) *Event {
|
||||||
func (e *Event) Timing(f func()) *Event {
|
func (e *Event) Timing(f func()) *Event {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
f()
|
f()
|
||||||
return e.Field(fieldTimeTaken, time.Since(start).Milliseconds())
|
return e.Field(timeTakenField, time.Since(start).Milliseconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err adds an "error" field to the log event
|
// Err adds an "error" field to the log event
|
||||||
|
@ -96,7 +95,7 @@ func (e *Event) Err(err error) *Event {
|
||||||
} else if c, ok := err.(Contexter); ok {
|
} else if c, ok := err.(Contexter); ok {
|
||||||
return e.With(c)
|
return e.With(c)
|
||||||
}
|
}
|
||||||
return e.Field(fieldError, err.Error())
|
return e.Field(errorField, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field adds a custom field and value to the log event
|
// Field adds a custom field and value to the log event
|
||||||
|
@ -108,14 +107,6 @@ func (e *Event) Field(key string, value any) *Event {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// FieldIf adds a custom field and value to the log event if the given level is loggable
|
|
||||||
func (e *Event) FieldIf(key string, value any, level Level) *Event {
|
|
||||||
if e.Loggable(level) {
|
|
||||||
return e.Field(key, value)
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields adds a map of fields to the log event
|
// Fields adds a map of fields to the log event
|
||||||
func (e *Event) Fields(fields Context) *Event {
|
func (e *Event) Fields(fields Context) *Event {
|
||||||
if e.fields == nil {
|
if e.fields == nil {
|
||||||
|
@ -127,46 +118,39 @@ func (e *Event) Fields(fields Context) *Event {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// With adds the fields of the given Contexter structs to the log event by calling their Context method
|
// With adds the fields of the given Contexter structs to the log event by calling their With method
|
||||||
func (e *Event) With(contexters ...Contexter) *Event {
|
func (e *Event) With(contexts ...Contexter) *Event {
|
||||||
if e.contexters == nil {
|
if e.contexters == nil {
|
||||||
e.contexters = contexters
|
e.contexters = contexts
|
||||||
} else {
|
} else {
|
||||||
e.contexters = append(e.contexters, contexters...)
|
e.contexters = append(e.contexters, contexts...)
|
||||||
}
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render returns the rendered log event as a string, or an empty string. The event is only rendered,
|
// maybeLog logs the event to the defined output. The event is only logged, if
|
||||||
// if either the global log level is >= l, or if the log level in one of the overrides matches
|
// either the global log level is >= l, or if the log level in one of the overrides matches
|
||||||
// the level.
|
// the level.
|
||||||
//
|
//
|
||||||
// If no overrides are defined (default), the Contexter array is not applied unless the event
|
// If no overrides are defined (default), the Contexter array is not applied unless the event
|
||||||
// is actually logged. If overrides are defined, then Contexters have to be applied in any case
|
// is actually logged. If overrides are defined, then Contexters have to be applied in any case
|
||||||
// to determine if they match. This is super complicated, but required for efficiency.
|
// to determine if they match. This is super complicated, but required for efficiency.
|
||||||
func (e *Event) Render(l Level, message string, v ...any) string {
|
func (e *Event) maybeLog(l Level, message string, v ...any) {
|
||||||
appliedContexters := e.maybeApplyContexters()
|
appliedContexters := e.maybeApplyContexters()
|
||||||
if !e.Loggable(l) {
|
if !e.shouldLog(l) {
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
e.Message = fmt.Sprintf(message, v...)
|
e.Message = fmt.Sprintf(message, v...)
|
||||||
e.Level = l
|
e.Level = l
|
||||||
e.Timestamp = util.FormatTime(e.time)
|
e.Timestamp = e.time.Format(timestampFormat)
|
||||||
if !appliedContexters {
|
if !appliedContexters {
|
||||||
e.applyContexters()
|
e.applyContexters()
|
||||||
}
|
}
|
||||||
if CurrentFormat() == JSONFormat {
|
if CurrentFormat() == JSONFormat {
|
||||||
return e.JSON()
|
log.Println(e.JSON())
|
||||||
|
} else {
|
||||||
|
log.Println(e.String())
|
||||||
}
|
}
|
||||||
return e.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log logs the event to the defined output, or does nothing if Render returns an empty string
|
|
||||||
func (e *Event) Log(l Level, message string, v ...any) *Event {
|
|
||||||
if m := e.Render(l, message, v...); m != "" {
|
|
||||||
log.Println(m)
|
|
||||||
}
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loggable returns true if the given log level is lower or equal to the current log level
|
// Loggable returns true if the given log level is lower or equal to the current log level
|
||||||
|
@ -208,6 +192,10 @@ func (e *Event) String() string {
|
||||||
return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", "))
|
return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Event) shouldLog(l Level) bool {
|
||||||
|
return e.globalLevelWithOverride() <= l
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Event) globalLevelWithOverride() Level {
|
func (e *Event) globalLevelWithOverride() Level {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
l, ov := level, overrides
|
l, ov := level, overrides
|
||||||
|
@ -215,13 +203,11 @@ func (e *Event) globalLevelWithOverride() Level {
|
||||||
if e.fields == nil {
|
if e.fields == nil {
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
for field, fieldOverrides := range ov {
|
for field, override := range ov {
|
||||||
value, exists := e.fields[field]
|
value, exists := e.fields[field]
|
||||||
if exists {
|
if exists {
|
||||||
for _, o := range fieldOverrides {
|
if override.value == "" || override.value == value || override.value == fmt.Sprintf("%v", value) {
|
||||||
if o.value == "" || o.value == value || o.value == fmt.Sprintf("%v", value) {
|
return override.level
|
||||||
return o.level
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
57
log/log.go
|
@ -4,7 +4,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -13,26 +12,17 @@ import (
|
||||||
var (
|
var (
|
||||||
DefaultLevel = InfoLevel
|
DefaultLevel = InfoLevel
|
||||||
DefaultFormat = TextFormat
|
DefaultFormat = TextFormat
|
||||||
DefaultOutput = &peekLogWriter{os.Stderr}
|
DefaultOutput = os.Stderr
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
level = DefaultLevel
|
level = DefaultLevel
|
||||||
format = DefaultFormat
|
format = DefaultFormat
|
||||||
overrides = make(map[string][]*levelOverride)
|
overrides = make(map[string]*levelOverride)
|
||||||
output io.Writer = DefaultOutput
|
output io.Writer = DefaultOutput
|
||||||
filename = ""
|
|
||||||
mu = &sync.RWMutex{}
|
mu = &sync.RWMutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// init sets the default log output (including log.SetOutput)
|
|
||||||
//
|
|
||||||
// This has to be explicitly called, because DefaultOutput is a peekLogWriter,
|
|
||||||
// which wraps os.Stderr.
|
|
||||||
func init() {
|
|
||||||
SetOutput(DefaultOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal prints the given message, and exits the program
|
// Fatal prints the given message, and exits the program
|
||||||
func Fatal(message string, v ...any) {
|
func Fatal(message string, v ...any) {
|
||||||
newEvent().Fatal(message, v...)
|
newEvent().Fatal(message, v...)
|
||||||
|
@ -111,17 +101,14 @@ func SetLevel(newLevel Level) {
|
||||||
func SetLevelOverride(field string, value string, level Level) {
|
func SetLevelOverride(field string, value string, level Level) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
if _, ok := overrides[field]; !ok {
|
overrides[field] = &levelOverride{value: value, level: level}
|
||||||
overrides[field] = make([]*levelOverride, 0)
|
|
||||||
}
|
|
||||||
overrides[field] = append(overrides[field], &levelOverride{value: value, level: level})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetLevelOverrides removes all log level overrides
|
// ResetLevelOverrides removes all log level overrides
|
||||||
func ResetLevelOverrides() {
|
func ResetLevelOverrides() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
overrides = make(map[string][]*levelOverride)
|
overrides = make(map[string]*levelOverride)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentFormat returns the current log format
|
// CurrentFormat returns the current log format
|
||||||
|
@ -145,27 +132,28 @@ func SetFormat(newFormat Format) {
|
||||||
func SetOutput(w io.Writer) {
|
func SetOutput(w io.Writer) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
output = &peekLogWriter{w}
|
log.SetOutput(w)
|
||||||
if f, ok := w.(*os.File); ok {
|
output = w
|
||||||
filename = f.Name()
|
|
||||||
} else {
|
|
||||||
filename = ""
|
|
||||||
}
|
|
||||||
log.SetOutput(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File returns the log file, if any, or an empty string otherwise
|
// File returns the log file, if any, or an empty string otherwise
|
||||||
func File() string {
|
func File() string {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
return filename
|
if f, ok := output.(*os.File); ok {
|
||||||
|
return f.Name()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFile returns true if the output is a non-default file
|
// IsFile returns true if the output is a non-default file
|
||||||
func IsFile() bool {
|
func IsFile() bool {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
return filename != ""
|
if _, ok := output.(*os.File); ok && output != DefaultOutput {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableDates disables the date/time prefix
|
// DisableDates disables the date/time prefix
|
||||||
|
@ -187,20 +175,3 @@ func IsTrace() bool {
|
||||||
func IsDebug() bool {
|
func IsDebug() bool {
|
||||||
return Loggable(DebugLevel)
|
return Loggable(DebugLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// peekLogWriter is an io.Writer which will peek at the rendered log event,
|
|
||||||
// and ensure that the rendered output is valid JSON. This is a hack!
|
|
||||||
type peekLogWriter struct {
|
|
||||||
w io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *peekLogWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if len(p) == 0 || p[0] == '{' || CurrentFormat() == TextFormat {
|
|
||||||
return w.w.Write(p)
|
|
||||||
}
|
|
||||||
m := newEvent().Tag(tagStdLog).Render(InfoLevel, strings.TrimSpace(string(p)))
|
|
||||||
if m == "" {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return w.w.Write([]byte(m + "\n"))
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,10 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -173,96 +170,6 @@ func TestLog_LevelOverrideAny(t *testing.T) {
|
||||||
{"time":"1970-01-01T00:00:14Z","level":"INFO","message":"this is also logged","time_taken_ms":0}
|
{"time":"1970-01-01T00:00:14Z","level":"INFO","message":"this is also logged","time_taken_ms":0}
|
||||||
`
|
`
|
||||||
require.Equal(t, expected, out.String())
|
require.Equal(t, expected, out.String())
|
||||||
require.False(t, IsFile())
|
|
||||||
require.Equal(t, "", File())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLog_LevelOverride_ManyOnSameField(t *testing.T) {
|
|
||||||
t.Cleanup(resetState)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
SetOutput(&out)
|
|
||||||
SetFormat(JSONFormat)
|
|
||||||
SetLevelOverride("tag", "manager", DebugLevel)
|
|
||||||
SetLevelOverride("tag", "publish", DebugLevel)
|
|
||||||
|
|
||||||
Time(time.Unix(11, 0).UTC()).Field("tag", "manager").Debug("this is logged")
|
|
||||||
Time(time.Unix(12, 0).UTC()).Field("tag", "no-match").Debug("this is not logged")
|
|
||||||
Time(time.Unix(13, 0).UTC()).Field("tag", "publish").Info("this is also logged")
|
|
||||||
|
|
||||||
expected := `{"time":"1970-01-01T00:00:11Z","level":"DEBUG","message":"this is logged","tag":"manager"}
|
|
||||||
{"time":"1970-01-01T00:00:13Z","level":"INFO","message":"this is also logged","tag":"publish"}
|
|
||||||
`
|
|
||||||
require.Equal(t, expected, out.String())
|
|
||||||
require.False(t, IsFile())
|
|
||||||
require.Equal(t, "", File())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLog_FieldIf(t *testing.T) {
|
|
||||||
t.Cleanup(resetState)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
SetOutput(&out)
|
|
||||||
SetLevel(DebugLevel)
|
|
||||||
SetFormat(JSONFormat)
|
|
||||||
|
|
||||||
Time(time.Unix(11, 0).UTC()).
|
|
||||||
FieldIf("trace_field", "manager", TraceLevel). // This is not logged
|
|
||||||
Field("tag", "manager").
|
|
||||||
Debug("trace_field is not logged")
|
|
||||||
SetLevel(TraceLevel)
|
|
||||||
Time(time.Unix(12, 0).UTC()).
|
|
||||||
FieldIf("trace_field", "manager", TraceLevel). // Now it is logged
|
|
||||||
Field("tag", "manager").
|
|
||||||
Debug("trace_field is logged")
|
|
||||||
|
|
||||||
expected := `{"time":"1970-01-01T00:00:11Z","level":"DEBUG","message":"trace_field is not logged","tag":"manager"}
|
|
||||||
{"time":"1970-01-01T00:00:12Z","level":"DEBUG","message":"trace_field is logged","tag":"manager","trace_field":"manager"}
|
|
||||||
`
|
|
||||||
require.Equal(t, expected, out.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLog_UsingStdLogger_JSON(t *testing.T) {
|
|
||||||
t.Cleanup(resetState)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
SetOutput(&out)
|
|
||||||
SetFormat(JSONFormat)
|
|
||||||
|
|
||||||
log.Println("Some other library is using the standard Go logger")
|
|
||||||
require.Contains(t, out.String(), `,"level":"INFO","message":"Some other library is using the standard Go logger","tag":"stdlog"}`+"\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLog_UsingStdLogger_Text(t *testing.T) {
|
|
||||||
t.Cleanup(resetState)
|
|
||||||
|
|
||||||
var out bytes.Buffer
|
|
||||||
SetOutput(&out)
|
|
||||||
|
|
||||||
log.Println("Some other library is using the standard Go logger")
|
|
||||||
require.Contains(t, out.String(), `Some other library is using the standard Go logger`+"\n")
|
|
||||||
require.NotContains(t, out.String(), `{`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLog_File(t *testing.T) {
|
|
||||||
t.Cleanup(resetState)
|
|
||||||
|
|
||||||
logfile := filepath.Join(t.TempDir(), "ntfy.log")
|
|
||||||
f, err := os.OpenFile(logfile, os.O_CREATE|os.O_WRONLY, 0600)
|
|
||||||
require.Nil(t, err)
|
|
||||||
SetOutput(f)
|
|
||||||
SetFormat(JSONFormat)
|
|
||||||
require.True(t, IsFile())
|
|
||||||
require.Equal(t, logfile, File())
|
|
||||||
|
|
||||||
Time(time.Unix(11, 0).UTC()).Field("this_one", "11").Info("this is logged")
|
|
||||||
require.Nil(t, f.Close())
|
|
||||||
|
|
||||||
f, err = os.Open(logfile)
|
|
||||||
require.Nil(t, err)
|
|
||||||
contents, err := io.ReadAll(f)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, `{"time":"1970-01-01T00:00:11Z","level":"INFO","message":"this is logged","this_one":"11"}`+"\n", string(contents))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeError struct {
|
type fakeError struct {
|
||||||
|
|
|
@ -102,13 +102,6 @@ type Contexter interface {
|
||||||
// Context represents an object's state in the form of key-value pairs
|
// Context represents an object's state in the form of key-value pairs
|
||||||
type Context map[string]any
|
type Context map[string]any
|
||||||
|
|
||||||
// Merge merges other into this context
|
|
||||||
func (c Context) Merge(other Context) {
|
|
||||||
for k, v := range other {
|
|
||||||
c[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type levelOverride struct {
|
type levelOverride struct {
|
||||||
value string
|
value string
|
||||||
level Level
|
level Level
|
||||||
|
|