Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2c2bd1e4b | ||
|
|
b003d79ae4 | ||
|
|
a52b024807 | ||
|
|
12b83828bd | ||
|
|
96bb357435 | ||
|
|
6a43c1a126 | ||
|
|
4dabc56952 | ||
|
|
5e510a19a1 | ||
|
|
b627a327d1 | ||
|
|
0b38efd761 | ||
|
|
983dec801a | ||
|
|
01eeb71b9d | ||
|
|
6ba1d7b2a5 | ||
|
|
ff202a042b | ||
|
|
af76a2606d | ||
|
|
98b56c2f06 | ||
|
|
b6afa2fd49 | ||
|
|
e1c07228e5 | ||
|
|
a949748d91 | ||
|
|
125fcd85bb | ||
|
|
2abd6a57ee | ||
|
|
35a691a1bc | ||
|
|
2bf6b8bb28 | ||
|
|
cb82e2690c | ||
|
|
ab1f9220a3 | ||
|
|
4c5d40e4c9 | ||
|
|
c33065151e | ||
|
|
ab01d0f04e | ||
|
|
42c3c6eb29 | ||
|
|
da7a1f656f | ||
|
|
63719ca0a0 | ||
|
|
cd27d47f4b | ||
|
|
60c5ccf34b | ||
|
|
d819de2626 | ||
|
|
79cb082879 | ||
|
|
632bf8d0b6 | ||
|
|
5e1d1698ff | ||
|
|
396497fff7 | ||
|
|
94bfe029d5 | ||
|
|
7f736eb93e | ||
|
|
414e283b46 | ||
|
|
a52e628f7b | ||
|
|
3eea97109e | ||
|
|
1950fc518f | ||
|
|
b93d654aca | ||
|
|
91594faf28 | ||
|
|
6c2aa0c3c2 | ||
|
|
db613f81cc | ||
|
|
51769c4094 | ||
|
|
cb768ca3f8 | ||
|
|
433e8e5b99 | ||
|
|
6cb42fbca1 | ||
|
|
406c172230 | ||
|
|
b4fbe81bb4 | ||
|
|
28f211bfef | ||
|
|
891257cce8 | ||
|
|
4cae237b36 | ||
|
|
c684a39191 | ||
|
|
797e4640df | ||
|
|
9684629549 | ||
|
|
a2825880bc | ||
|
|
577cd0fcea | ||
|
|
4b86085a8c | ||
|
|
0ee99e10c8 | ||
|
|
fe96110e6b | ||
|
|
35f173e17c | ||
|
|
87f8af9b97 | ||
|
|
4dd215d3d8 | ||
|
|
5a8818ac92 | ||
|
|
3baa93a0d4 | ||
|
|
72ec2f9988 | ||
|
|
ae3d063c2d | ||
|
|
d0bb27cf0c | ||
|
|
67be8e3ff8 | ||
|
|
4571ba1c24 | ||
|
|
6d601ad141 | ||
|
|
f63b15ba5a | ||
|
|
5c01d13fe3 | ||
|
|
19d2a46457 | ||
|
|
613348d37e | ||
|
|
7d473488de | ||
|
|
6e4b31b4e9 | ||
|
|
88474957a2 | ||
|
|
9dc532de30 | ||
|
|
fe37258bc2 | ||
|
|
5291e9be7f | ||
|
|
6ab02a31a2 | ||
|
|
14d9d120e6 | ||
|
|
f5981b851d | ||
|
|
c357979f11 | ||
|
|
6ee3349cca | ||
|
|
91e6eaab19 | ||
|
|
3973f1e5ed | ||
|
|
15ac5ed23b | ||
|
|
344da326cd | ||
|
|
cacfb704a4 | ||
|
|
040bb53383 | ||
|
|
5cac63bfbe | ||
|
|
8d908fe438 | ||
|
|
7db99d18c7 | ||
|
|
2bb5d6f934 | ||
|
|
bb13011046 | ||
|
|
8cc12e12da | ||
|
|
6e2b300d9e | ||
|
|
1197d72523 |
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
72
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ main ]
|
||||||
|
schedule:
|
||||||
|
- cron: '21 10 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go', 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,13 @@
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
server/docs/
|
server/docs/
|
||||||
server/site/
|
server/site/
|
||||||
tools/fbsend/fbsend
|
tools/fbsend/fbsend
|
||||||
playground/
|
playground/
|
||||||
|
secrets/
|
||||||
*.iml
|
*.iml
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ before:
|
|||||||
- go mod tidy
|
- go mod tidy
|
||||||
builds:
|
builds:
|
||||||
-
|
-
|
||||||
id: ntfy_amd64
|
id: ntfy_linux_amd64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1 # required for go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
@@ -17,7 +17,7 @@ builds:
|
|||||||
post:
|
post:
|
||||||
- upx "{{ .Path }}" # apt install upx
|
- upx "{{ .Path }}" # apt install upx
|
||||||
-
|
-
|
||||||
id: ntfy_armv6
|
id: ntfy_linux_armv6
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1 # required for go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
@@ -28,10 +28,9 @@ builds:
|
|||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm]
|
goarch: [arm]
|
||||||
goarm: [6]
|
goarm: [6]
|
||||||
# No "upx", since it causes random core dumps, see
|
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||||
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
|
||||||
-
|
-
|
||||||
id: ntfy_armv7
|
id: ntfy_linux_armv7
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1 # required for go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
@@ -42,10 +41,9 @@ builds:
|
|||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm]
|
goarch: [arm]
|
||||||
goarm: [7]
|
goarm: [7]
|
||||||
# No "upx", since it causes random core dumps, see
|
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||||
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
|
||||||
-
|
-
|
||||||
id: ntfy_arm64
|
id: ntfy_linux_arm64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=1 # required for go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
@@ -55,8 +53,30 @@ builds:
|
|||||||
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [arm64]
|
goarch: [arm64]
|
||||||
# No "upx", since it causes random core dumps, see
|
# No "upx" for ARM, see https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
||||||
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
|
-
|
||||||
|
id: ntfy_windows_amd64
|
||||||
|
binary: ntfy
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||||
|
tags: [noserver] # don't include server files
|
||||||
|
ldflags:
|
||||||
|
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
|
goos: [windows]
|
||||||
|
goarch: [amd64]
|
||||||
|
hooks:
|
||||||
|
post:
|
||||||
|
- upx "{{ .Path }}" # apt install upx
|
||||||
|
-
|
||||||
|
id: ntfy_darwin_all
|
||||||
|
binary: ntfy
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||||
|
tags: [noserver] # don't include server files
|
||||||
|
ldflags:
|
||||||
|
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
|
goos: [darwin]
|
||||||
|
goarch: [amd64, arm64] # will be combined to "universal binary" (see below)
|
||||||
nfpms:
|
nfpms:
|
||||||
-
|
-
|
||||||
package_name: ntfy
|
package_name: ntfy
|
||||||
@@ -94,6 +114,12 @@ nfpms:
|
|||||||
postremove: "scripts/postrm.sh"
|
postremove: "scripts/postrm.sh"
|
||||||
archives:
|
archives:
|
||||||
-
|
-
|
||||||
|
id: ntfy_linux
|
||||||
|
builds:
|
||||||
|
- ntfy_linux_amd64
|
||||||
|
- ntfy_linux_armv6
|
||||||
|
- ntfy_linux_armv7
|
||||||
|
- ntfy_linux_arm64
|
||||||
wrap_in_directory: true
|
wrap_in_directory: true
|
||||||
files:
|
files:
|
||||||
- LICENSE
|
- LICENSE
|
||||||
@@ -103,8 +129,34 @@ archives:
|
|||||||
- client/client.yml
|
- client/client.yml
|
||||||
- client/ntfy-client.service
|
- client/ntfy-client.service
|
||||||
replacements:
|
replacements:
|
||||||
386: i386
|
|
||||||
amd64: x86_64
|
amd64: x86_64
|
||||||
|
-
|
||||||
|
id: ntfy_windows
|
||||||
|
builds:
|
||||||
|
- ntfy_windows_amd64
|
||||||
|
format: zip
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- client/client.yml
|
||||||
|
replacements:
|
||||||
|
amd64: x86_64
|
||||||
|
-
|
||||||
|
id: ntfy_darwin
|
||||||
|
builds:
|
||||||
|
- ntfy_darwin_all
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- client/client.yml
|
||||||
|
replacements:
|
||||||
|
darwin: macOS
|
||||||
|
universal_binaries:
|
||||||
|
-
|
||||||
|
id: ntfy_darwin_all
|
||||||
|
replace: true
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
snapshot:
|
snapshot:
|
||||||
|
|||||||
198
Makefile
198
Makefile
@@ -1,64 +1,72 @@
|
|||||||
|
MAKEFLAGS := --jobs=1
|
||||||
VERSION := $(shell git describe --tag)
|
VERSION := $(shell git describe --tag)
|
||||||
|
|
||||||
.PHONY:
|
.PHONY:
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Typical commands (more see below):"
|
@echo "Typical commands (more see below):"
|
||||||
@echo " make build - Build web app, documentation and server/client (sloowwww)"
|
@echo " make build - Build web app, documentation and server/client (sloowwww)"
|
||||||
@echo " make server-amd64 - Build server/client binary (amd64, no web app or docs)"
|
@echo " make cli-linux-amd64 - Build server/client binary (amd64, no web app or docs)"
|
||||||
@echo " make install-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)"
|
@echo " make install-linux-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)"
|
||||||
@echo " make web - Build the web app"
|
@echo " make web - Build the web app"
|
||||||
@echo " make docs - Build the documentation"
|
@echo " make docs - Build the documentation"
|
||||||
@echo " make check - Run all tests, vetting/formatting checks and linters"
|
@echo " make check - Run all tests, vetting/formatting checks and linters"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build everything:"
|
@echo "Build everything:"
|
||||||
@echo " make build - Build web app, documentation and server/client"
|
@echo " make build - Build web app, documentation and server/client"
|
||||||
@echo " make clean - Clean build/dist folders"
|
@echo " make clean - Clean build/dist folders"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build server & client (not release version):"
|
@echo "Build server & client (using GoReleaser, not release version):"
|
||||||
@echo " make server - Build server & client (all architectures)"
|
@echo " make cli - Build server & client (all architectures)"
|
||||||
@echo " make server-amd64 - Build server & client (amd64 only)"
|
@echo " make cli-linux-amd64 - Build server & client (Linux, amd64 only)"
|
||||||
@echo " make server-armv6 - Build server & client (armv6 only)"
|
@echo " make cli-linux-armv6 - Build server & client (Linux, armv6 only)"
|
||||||
@echo " make server-armv7 - Build server & client (armv7 only)"
|
@echo " make cli-linux-armv7 - Build server & client (Linux, armv7 only)"
|
||||||
@echo " make server-arm64 - Build server & client (arm64 only)"
|
@echo " make cli-linux-arm64 - Build server & client (Linux, arm64 only)"
|
||||||
|
@echo " make cli-windows-amd64 - Build client (Windows, amd64 only)"
|
||||||
|
@echo " make cli-darwin-all - Build client (macOS, arm64+amd64 universal binary)"
|
||||||
|
@echo
|
||||||
|
@echo "Build server & client (without GoReleaser):"
|
||||||
|
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
||||||
|
@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
|
@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
|
@echo
|
||||||
@echo "Build documentation:"
|
@echo "Build documentation:"
|
||||||
@echo " make docs - Build the documentation"
|
@echo " make docs - Build the documentation"
|
||||||
@echo " make docs-deps - Install Python dependencies (pip3 install)"
|
@echo " make docs-deps - Install Python dependencies (pip3 install)"
|
||||||
@echo " make docs-build - Actually build the documentation"
|
@echo " make docs-build - Actually build the documentation"
|
||||||
@echo
|
@echo
|
||||||
@echo "Test/check:"
|
@echo "Test/check:"
|
||||||
@echo " make test - Run tests"
|
@echo " make test - Run tests"
|
||||||
@echo " make race - Run tests with -race flag"
|
@echo " make race - Run tests with -race flag"
|
||||||
@echo " make coverage - Run tests and show coverage"
|
@echo " make coverage - Run tests and show coverage"
|
||||||
@echo " make coverage-html - Run tests and show coverage (as HTML)"
|
@echo " make coverage-html - Run tests and show coverage (as HTML)"
|
||||||
@echo " make coverage-upload - Upload coverage results to codecov.io"
|
@echo " make coverage-upload - Upload coverage results to codecov.io"
|
||||||
@echo
|
@echo
|
||||||
@echo "Lint/format:"
|
@echo "Lint/format:"
|
||||||
@echo " make fmt - Run 'go fmt'"
|
@echo " make fmt - Run 'go fmt'"
|
||||||
@echo " make fmt-check - Run 'go fmt', but don't change anything"
|
@echo " make fmt-check - Run 'go fmt', but don't change anything"
|
||||||
@echo " make vet - Run 'go vet'"
|
@echo " make vet - Run 'go vet'"
|
||||||
@echo " make lint - Run 'golint'"
|
@echo " make lint - Run 'golint'"
|
||||||
@echo " make staticcheck - Run 'staticcheck'"
|
@echo " make staticcheck - Run 'staticcheck'"
|
||||||
@echo
|
@echo
|
||||||
@echo "Releasing:"
|
@echo "Releasing:"
|
||||||
@echo " make release - Create a release"
|
@echo " make release - Create a release"
|
||||||
@echo " make release-snapshot - Create a test release"
|
@echo " make release-snapshot - Create a test release"
|
||||||
@echo
|
@echo
|
||||||
@echo "Install locally (requires sudo):"
|
@echo "Install locally (requires sudo):"
|
||||||
@echo " make install-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
|
@echo " make install-linux-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
|
||||||
@echo " make install-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
|
@echo " make install-linux-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
|
||||||
@echo " make install-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
|
@echo " make install-linux-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
|
||||||
@echo " make install-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
|
@echo " make install-linux-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
|
||||||
@echo " make install-deb-amd64 - Install .deb from dist/ (amd64 only)"
|
@echo " make install-linux-deb-amd64 - Install .deb from dist/ (amd64 only)"
|
||||||
@echo " make install-deb-armv6 - Install .deb from dist/ (armv6 only)"
|
@echo " make install-linux-deb-armv6 - Install .deb from dist/ (armv6 only)"
|
||||||
@echo " make install-deb-armv7 - Install .deb from dist/ (armv7 only)"
|
@echo " make install-linux-deb-armv7 - Install .deb from dist/ (armv7 only)"
|
||||||
@echo " make install-deb-arm64 - Install .deb from dist/ (arm64 only)"
|
@echo " make install-linux-deb-arm64 - Install .deb from dist/ (arm64 only)"
|
||||||
|
|
||||||
|
|
||||||
# Building everything
|
# Building everything
|
||||||
@@ -66,28 +74,29 @@ help:
|
|||||||
clean: .PHONY
|
clean: .PHONY
|
||||||
rm -rf dist build server/docs server/site
|
rm -rf dist build server/docs server/site
|
||||||
|
|
||||||
build: web docs server
|
build: web docs cli
|
||||||
|
|
||||||
|
update: web-deps-update cli-deps-update docs-deps-update
|
||||||
|
docker pull alpine
|
||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
docs: docs-deps docs-build
|
docs: docs-deps docs-build
|
||||||
|
|
||||||
|
docs-build: .PHONY
|
||||||
|
mkdocs build
|
||||||
|
|
||||||
docs-deps: .PHONY
|
docs-deps: .PHONY
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
docs-build: .PHONY
|
docs-deps-update: .PHONY
|
||||||
mkdocs build
|
pip3 install -r requirements.txt --upgrade
|
||||||
|
|
||||||
|
|
||||||
# Web app
|
# Web app
|
||||||
|
|
||||||
web: web-deps web-build
|
web: web-deps web-build
|
||||||
|
|
||||||
web-deps:
|
|
||||||
cd web && npm install
|
|
||||||
# If this fails for .svg files, optimizes them with svgo
|
|
||||||
|
|
||||||
web-build:
|
web-build:
|
||||||
cd web \
|
cd web \
|
||||||
&& npm run build \
|
&& npm run build \
|
||||||
@@ -98,41 +107,86 @@ web-build:
|
|||||||
../server/site/config.js \
|
../server/site/config.js \
|
||||||
../server/site/asset-manifest.json
|
../server/site/asset-manifest.json
|
||||||
|
|
||||||
|
web-deps:
|
||||||
|
cd web && npm install
|
||||||
|
# If this fails for .svg files, optimize them with svgo
|
||||||
|
|
||||||
|
web-deps-update:
|
||||||
|
cd web && npm update
|
||||||
|
|
||||||
# Main server/client build
|
# Main server/client build
|
||||||
|
|
||||||
server: server-deps
|
cli: cli-deps
|
||||||
goreleaser build --snapshot --rm-dist --debug
|
goreleaser build --snapshot --rm-dist --debug
|
||||||
|
|
||||||
server-amd64: server-deps-static-sites
|
cli-linux-amd64: cli-deps-static-sites
|
||||||
goreleaser build --snapshot --rm-dist --debug --id ntfy_amd64
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_amd64
|
||||||
|
|
||||||
server-armv6: server-deps-static-sites server-deps-gcc-armv6-armv7
|
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||||
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv6
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv6
|
||||||
|
|
||||||
server-armv7: server-deps-static-sites server-deps-gcc-armv6-armv7
|
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
|
||||||
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv7
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv7
|
||||||
|
|
||||||
server-arm64: server-deps-static-sites server-deps-gcc-arm64
|
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
|
||||||
goreleaser build --snapshot --rm-dist --debug --id ntfy_arm64
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_arm64
|
||||||
|
|
||||||
server-deps: server-deps-static-sites server-deps-all server-deps-gcc
|
cli-windows-amd64: cli-deps-static-sites
|
||||||
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_windows_amd64
|
||||||
|
|
||||||
server-deps-gcc: server-deps-gcc-armv6-armv7 server-deps-gcc-arm64
|
cli-darwin-all: cli-deps-static-sites
|
||||||
|
goreleaser build --snapshot --rm-dist --debug --id ntfy_darwin_all
|
||||||
|
|
||||||
server-deps-static-sites:
|
cli-linux-server: cli-deps-static-sites
|
||||||
|
# This is a target to build the CLI (including the server) manually.
|
||||||
|
# Use this for development, if you really don't want to install GoReleaser ...
|
||||||
|
mkdir -p dist/ntfy_linux_server server/docs
|
||||||
|
CGO_ENABLED=1 go build \
|
||||||
|
-o dist/ntfy_linux_server/ntfy \
|
||||||
|
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||||
|
-ldflags \
|
||||||
|
"-linkmode=external -extldflags=-static -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
|
cli-darwin-server: cli-deps-static-sites
|
||||||
|
# This is a target to build the CLI (including the server) manually.
|
||||||
|
# Use this for macOS/iOS development, so you have a local server to test with.
|
||||||
|
mkdir -p dist/ntfy_darwin_server server/docs
|
||||||
|
CGO_ENABLED=1 go build \
|
||||||
|
-o dist/ntfy_darwin_server/ntfy \
|
||||||
|
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||||
|
-ldflags \
|
||||||
|
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
|
cli-client: cli-deps-static-sites
|
||||||
|
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
||||||
|
# Use this for development, if you really don't want to install GoReleaser ...
|
||||||
|
mkdir -p dist/ntfy_client server/docs
|
||||||
|
CGO_ENABLED=0 go build \
|
||||||
|
-o dist/ntfy_client/ntfy \
|
||||||
|
-tags noserver \
|
||||||
|
-ldflags \
|
||||||
|
"-X main.version=$(VERSION) -X main.commit=$(shell git rev-parse --short HEAD) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
|
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
||||||
|
|
||||||
|
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64
|
||||||
|
|
||||||
|
cli-deps-static-sites:
|
||||||
mkdir -p server/docs server/site
|
mkdir -p server/docs server/site
|
||||||
touch server/docs/index.html server/site/app.html
|
touch server/docs/index.html server/site/app.html
|
||||||
|
|
||||||
server-deps-all:
|
cli-deps-all:
|
||||||
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
|
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
|
||||||
|
|
||||||
server-deps-gcc-armv6-armv7:
|
cli-deps-gcc-armv6-armv7:
|
||||||
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
|
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
|
||||||
|
|
||||||
server-deps-gcc-arm64:
|
cli-deps-gcc-arm64:
|
||||||
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
||||||
|
|
||||||
|
cli-deps-update:
|
||||||
|
go get -u
|
||||||
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|
||||||
# Test/check targets
|
# Test/check targets
|
||||||
|
|
||||||
@@ -184,10 +238,10 @@ staticcheck: .PHONY
|
|||||||
|
|
||||||
# Releasing targets
|
# Releasing targets
|
||||||
|
|
||||||
release: clean server-deps release-check-tags docs web check
|
release: clean update cli-deps release-check-tags docs web check
|
||||||
goreleaser release --rm-dist --debug
|
goreleaser release --rm-dist --debug
|
||||||
|
|
||||||
release-snapshot: clean server-deps docs web check
|
release-snapshot: clean update cli-deps docs web check
|
||||||
goreleaser release --snapshot --skip-publish --rm-dist --debug
|
goreleaser release --snapshot --skip-publish --rm-dist --debug
|
||||||
|
|
||||||
release-check-tags:
|
release-check-tags:
|
||||||
@@ -204,31 +258,31 @@ release-check-tags:
|
|||||||
|
|
||||||
# Installing targets
|
# Installing targets
|
||||||
|
|
||||||
install-amd64: remove-binary
|
install-linux-amd64: remove-binary
|
||||||
sudo cp -a dist/ntfy_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy
|
sudo cp -a dist/ntfy_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy
|
||||||
|
|
||||||
install-armv6: remove-binary
|
install-linux-armv6: remove-binary
|
||||||
sudo cp -a dist/ntfy_armv6_linux_arm_6/ntfy /usr/bin/ntfy
|
sudo cp -a dist/ntfy_armv6_linux_arm_6/ntfy /usr/bin/ntfy
|
||||||
|
|
||||||
install-armv7: remove-binary
|
install-linux-armv7: remove-binary
|
||||||
sudo cp -a dist/ntfy_armv7_linux_arm_7/ntfy /usr/bin/ntfy
|
sudo cp -a dist/ntfy_armv7_linux_arm_7/ntfy /usr/bin/ntfy
|
||||||
|
|
||||||
install-arm64: remove-binary
|
install-linux-arm64: remove-binary
|
||||||
sudo cp -a dist/ntfy_arm64_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a dist/ntfy_arm64_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
|
|
||||||
remove-binary:
|
remove-binary:
|
||||||
sudo rm -f /usr/bin/ntfy
|
sudo rm -f /usr/bin/ntfy
|
||||||
|
|
||||||
install-amd64-deb: purge-package
|
install-linux-amd64-deb: purge-package
|
||||||
sudo dpkg -i dist/ntfy_*_linux_amd64.deb
|
sudo dpkg -i dist/ntfy_*_linux_amd64.deb
|
||||||
|
|
||||||
install-armv6-deb: purge-package
|
install-linux-armv6-deb: purge-package
|
||||||
sudo dpkg -i dist/ntfy_*_linux_armv6.deb
|
sudo dpkg -i dist/ntfy_*_linux_armv6.deb
|
||||||
|
|
||||||
install-armv7-deb: purge-package
|
install-linux-armv7-deb: purge-package
|
||||||
sudo dpkg -i dist/ntfy_*_linux_armv7.deb
|
sudo dpkg -i dist/ntfy_*_linux_armv7.deb
|
||||||
|
|
||||||
install-arm64-deb: purge-package
|
install-linux-arm64-deb: purge-package
|
||||||
sudo dpkg -i dist/ntfy_*_linux_arm64.deb
|
sudo dpkg -i dist/ntfy_*_linux_arm64.deb
|
||||||
|
|
||||||
purge-package:
|
purge-package:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !noserver
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,6 +10,10 @@ import (
|
|||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdAccess)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
userEveryone = "everyone"
|
userEveryone = "everyone"
|
||||||
)
|
)
|
||||||
@@ -22,7 +28,7 @@ var cmdAccess = &cli.Command{
|
|||||||
Usage: "Grant/revoke access to a topic, or show access",
|
Usage: "Grant/revoke access to a topic, or show access",
|
||||||
UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]",
|
UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]",
|
||||||
Flags: flagsAccess,
|
Flags: flagsAccess,
|
||||||
Before: initConfigFileInputSource("config", flagsAccess),
|
Before: initConfigFileInputSourceFunc("config", flagsAccess),
|
||||||
Action: execUserAccess,
|
Action: execUserAccess,
|
||||||
Category: categoryServer,
|
Category: categoryServer,
|
||||||
Description: `Manage the access control list for the ntfy server.
|
Description: `Manage the access control list for the ntfy server.
|
||||||
|
|||||||
39
cmd/app.go
39
cmd/app.go
@@ -2,23 +2,17 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
defaultClientRootConfigFile = "/etc/ntfy/client.yml"
|
|
||||||
defaultClientUserConfigFile = "~/.config/ntfy/client.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
categoryClient = "Client commands"
|
categoryClient = "Client commands"
|
||||||
categoryServer = "Server commands"
|
categoryServer = "Server commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var commands = make([]*cli.Command, 0)
|
||||||
|
|
||||||
// New creates a new CLI application
|
// New creates a new CLI application
|
||||||
func New() *cli.App {
|
func New() *cli.App {
|
||||||
return &cli.App{
|
return &cli.App{
|
||||||
@@ -30,33 +24,6 @@ func New() *cli.App {
|
|||||||
Reader: os.Stdin,
|
Reader: os.Stdin,
|
||||||
Writer: os.Stdout,
|
Writer: os.Stdout,
|
||||||
ErrWriter: os.Stderr,
|
ErrWriter: os.Stderr,
|
||||||
Commands: []*cli.Command{
|
Commands: commands,
|
||||||
// Server commands
|
|
||||||
cmdServe,
|
|
||||||
cmdUser,
|
|
||||||
cmdAccess,
|
|
||||||
|
|
||||||
// Client commands
|
|
||||||
cmdPublish,
|
|
||||||
cmdSubscribe,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
|
|
||||||
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
|
|
||||||
func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFunc {
|
|
||||||
return func(context *cli.Context) error {
|
|
||||||
configFile := context.String(configFlag)
|
|
||||||
if context.IsSet(configFlag) && !util.FileExists(configFile) {
|
|
||||||
return fmt.Errorf("config file %s does not exist", configFile)
|
|
||||||
} else if !context.IsSet(configFlag) && !util.FileExists(configFile) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
inputSource, err := altsrc.NewYamlSourceFromFile(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
cmd/config_loader.go
Normal file
52
cmd/config_loader.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initConfigFileInputSourceFunc is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
|
||||||
|
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
|
||||||
|
func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.BeforeFunc {
|
||||||
|
return func(context *cli.Context) error {
|
||||||
|
configFile := context.String(configFlag)
|
||||||
|
if context.IsSet(configFlag) && !util.FileExists(configFile) {
|
||||||
|
return fmt.Errorf("config file %s does not exist", configFile)
|
||||||
|
} else if !context.IsSet(configFlag) && !util.FileExists(configFile) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
inputSource, err := newYamlSourceFromFile(configFile, flags)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath.
|
||||||
|
//
|
||||||
|
// This function also maps aliases, so a .yml file can contain short options, or options with underscores
|
||||||
|
// instead of dashes. See https://github.com/binwiederhier/ntfy/issues/255.
|
||||||
|
func newYamlSourceFromFile(file string, flags []cli.Flag) (altsrc.InputSourceContext, error) {
|
||||||
|
var rawConfig map[interface{}]interface{}
|
||||||
|
b, err := os.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(b, &rawConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, f := range flags {
|
||||||
|
flagName := f.Names()[0]
|
||||||
|
for _, flagAlias := range f.Names()[1:] {
|
||||||
|
if _, ok := rawConfig[flagAlias]; ok {
|
||||||
|
rawConfig[flagName] = rawConfig[flagAlias]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return altsrc.NewMapInputSource(file, rawConfig), nil
|
||||||
|
}
|
||||||
38
cmd/config_loader_test.go
Normal file
38
cmd/config_loader_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewYamlSourceFromFile(t *testing.T) {
|
||||||
|
filename := filepath.Join(t.TempDir(), "server.yml")
|
||||||
|
contents := `
|
||||||
|
# Normal options
|
||||||
|
listen-https: ":10443"
|
||||||
|
|
||||||
|
# Note the underscore!
|
||||||
|
listen_http: ":1080"
|
||||||
|
|
||||||
|
# OMG this is allowed now ...
|
||||||
|
K: /some/file.pem
|
||||||
|
`
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(contents), 0600))
|
||||||
|
|
||||||
|
ctx, err := newYamlSourceFromFile(filename, flagsServe)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
listenHTTPS, err := ctx.String("listen-https")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, ":10443", listenHTTPS)
|
||||||
|
|
||||||
|
listenHTTP, err := ctx.String("listen-http") // No underscore!
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, ":1080", listenHTTP)
|
||||||
|
|
||||||
|
keyFile, err := ctx.String("key-file") // Long option!
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "/some/file.pem", keyFile)
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdPublish)
|
||||||
|
}
|
||||||
|
|
||||||
var cmdPublish = &cli.Command{
|
var cmdPublish = &cli.Command{
|
||||||
Name: "publish",
|
Name: "publish",
|
||||||
Aliases: []string{"pub", "send", "trigger"},
|
Aliases: []string{"pub", "send", "trigger"},
|
||||||
@@ -59,8 +63,7 @@ Examples:
|
|||||||
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
||||||
it has incredibly useful information: https://ntfy.sh/docs/publish/.
|
it has incredibly useful information: https://ntfy.sh/docs/publish/.
|
||||||
|
|
||||||
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
|
` + clientCommandDescriptionSuffix,
|
||||||
or ~/.config/ntfy/client.yml for all other users.`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func execPublish(c *cli.Context) error {
|
func execPublish(c *cli.Context) error {
|
||||||
|
|||||||
101
cmd/serve.go
101
cmd/serve.go
@@ -1,56 +1,64 @@
|
|||||||
|
//go:build !noserver
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdServe)
|
||||||
|
}
|
||||||
|
|
||||||
var flagsServe = []cli.Flag{
|
var flagsServe = []cli.Flag{
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"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{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to 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{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to 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{"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.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"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"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"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{"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.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
|
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.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
|
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: "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-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-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (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-from", EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (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-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-sender-from", Aliases: []string{"smtp_sender_from"}, EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "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-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-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-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
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.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
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.NewStringFlag(&cli.StringFlag{Name: "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.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-daily-bandwidth-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit 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.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"visitor_attachment_daily_bandwidth_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "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.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.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"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.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: "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)"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdServe = &cli.Command{
|
var cmdServe = &cli.Command{
|
||||||
@@ -60,7 +68,7 @@ var cmdServe = &cli.Command{
|
|||||||
Action: execServe,
|
Action: execServe,
|
||||||
Category: categoryServer,
|
Category: categoryServer,
|
||||||
Flags: flagsServe,
|
Flags: flagsServe,
|
||||||
Before: initConfigFileInputSource("config", flagsServe),
|
Before: initConfigFileInputSourceFunc("config", flagsServe),
|
||||||
Description: `Run the ntfy server and listen for incoming requests
|
Description: `Run the ntfy server and listen for incoming requests
|
||||||
|
|
||||||
The command will load the configuration from /etc/ntfy/server.yml. Config options can
|
The command will load the configuration from /etc/ntfy/server.yml. Config options can
|
||||||
@@ -95,6 +103,7 @@ func execServe(c *cli.Context) error {
|
|||||||
keepaliveInterval := c.Duration("keepalive-interval")
|
keepaliveInterval := c.Duration("keepalive-interval")
|
||||||
managerInterval := c.Duration("manager-interval")
|
managerInterval := c.Duration("manager-interval")
|
||||||
webRoot := c.String("web-root")
|
webRoot := c.String("web-root")
|
||||||
|
upstreamBaseURL := c.String("upstream-base-url")
|
||||||
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")
|
||||||
@@ -138,12 +147,18 @@ 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 !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
|
||||||
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
|
||||||
} else if !util.InStringList([]string{"app", "home"}, webRoot) {
|
} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
|
||||||
return errors.New("if set, web-root must be 'home' or 'app'")
|
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||||
|
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||||
|
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||||
|
} else if upstreamBaseURL != "" && baseURL == "" {
|
||||||
|
return errors.New("if upstream-base-url is set, base-url must also be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default auth permissions
|
|
||||||
webRootIsApp := webRoot == "app"
|
webRootIsApp := webRoot == "app"
|
||||||
|
enableWeb := webRoot != "disable"
|
||||||
|
|
||||||
|
// Default auth permissions
|
||||||
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
|
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
|
||||||
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
|
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
|
||||||
|
|
||||||
@@ -206,6 +221,7 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.KeepaliveInterval = keepaliveInterval
|
conf.KeepaliveInterval = keepaliveInterval
|
||||||
conf.ManagerInterval = managerInterval
|
conf.ManagerInterval = managerInterval
|
||||||
conf.WebRootIsApp = webRootIsApp
|
conf.WebRootIsApp = webRootIsApp
|
||||||
|
conf.UpstreamBaseURL = upstreamBaseURL
|
||||||
conf.SMTPSenderAddr = smtpSenderAddr
|
conf.SMTPSenderAddr = smtpSenderAddr
|
||||||
conf.SMTPSenderUser = smtpSenderUser
|
conf.SMTPSenderUser = smtpSenderUser
|
||||||
conf.SMTPSenderPass = smtpSenderPass
|
conf.SMTPSenderPass = smtpSenderPass
|
||||||
@@ -223,6 +239,7 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||||
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||||
conf.BehindProxy = behindProxy
|
conf.BehindProxy = behindProxy
|
||||||
|
conf.EnableWeb = enableWeb
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
|||||||
@@ -10,9 +10,20 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdSubscribe)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
|
||||||
|
clientUserConfigFileUnixRelative = "ntfy/client.yml"
|
||||||
|
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
|
||||||
|
)
|
||||||
|
|
||||||
var cmdSubscribe = &cli.Command{
|
var cmdSubscribe = &cli.Command{
|
||||||
Name: "subscribe",
|
Name: "subscribe",
|
||||||
Aliases: []string{"sub"},
|
Aliases: []string{"sub"},
|
||||||
@@ -60,19 +71,17 @@ ntfy subscribe TOPIC COMMAND
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
|
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
|
||||||
ntfy sub topic1 /my/script.sh # Execute script for incoming messages
|
ntfy sub topic1 myscript.sh # Execute script for incoming messages
|
||||||
|
|
||||||
ntfy subscribe --from-config
|
ntfy subscribe --from-config
|
||||||
Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml
|
Service mode (used in ntfy-client.service). This reads the config file and sets up
|
||||||
or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:"
|
subscriptions for every topic in the "subscribe:" block (see config file).
|
||||||
block (see config file).
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
ntfy sub --from-config # Read topics from config file
|
ntfy sub --from-config # Read topics from config file
|
||||||
ntfy sub --config=/my/client.yml --from-config # Read topics from alternate config file
|
ntfy sub --config=myclient.yml --from-config # Read topics from alternate config file
|
||||||
|
|
||||||
The default config file for all client commands is /etc/ntfy/client.yml (if root user),
|
` + clientCommandDescriptionSuffix,
|
||||||
or ~/.config/ntfy/client.yml for all other users.`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func execSubscribe(c *cli.Context) error {
|
func execSubscribe(c *cli.Context) error {
|
||||||
@@ -156,8 +165,8 @@ func doPollSingle(c *cli.Context, cl *client.Client, topic, command string, opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
||||||
commands := make(map[string]string) // Subscription ID -> command
|
cmds := make(map[string]string) // Subscription ID -> command
|
||||||
for _, s := range conf.Subscribe { // May be nil
|
for _, s := range conf.Subscribe { // May be nil
|
||||||
topicOptions := append(make([]client.SubscribeOption, 0), options...)
|
topicOptions := append(make([]client.SubscribeOption, 0), options...)
|
||||||
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))
|
||||||
@@ -166,18 +175,18 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
|||||||
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
|
topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password))
|
||||||
}
|
}
|
||||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||||
commands[subscriptionID] = s.Command
|
cmds[subscriptionID] = s.Command
|
||||||
}
|
}
|
||||||
if topic != "" {
|
if topic != "" {
|
||||||
subscriptionID := cl.Subscribe(topic, options...)
|
subscriptionID := cl.Subscribe(topic, options...)
|
||||||
commands[subscriptionID] = command
|
cmds[subscriptionID] = command
|
||||||
}
|
}
|
||||||
for m := range cl.Messages {
|
for m := range cl.Messages {
|
||||||
command, ok := commands[m.SubscriptionID]
|
cmd, ok := cmds[m.SubscriptionID]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
printMessageOrRunCommand(c, m, command)
|
printMessageOrRunCommand(c, m, cmd)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -196,17 +205,17 @@ func runCommand(c *cli.Context, command string, m *client.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
|
func runCommandInternal(c *cli.Context, script string, m *client.Message) error {
|
||||||
scriptFile, err := createTmpScript(command)
|
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.%s", os.TempDir(), util.RandomString(10), scriptExt)
|
||||||
if err != nil {
|
if err := os.WriteFile(scriptFile, []byte(scriptHeader+script), 0700); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer os.Remove(scriptFile)
|
defer os.Remove(scriptFile)
|
||||||
verbose := c.Bool("verbose")
|
verbose := c.Bool("verbose")
|
||||||
if verbose {
|
if verbose {
|
||||||
log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
|
log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), script, m.Raw)
|
||||||
}
|
}
|
||||||
cmd := exec.Command("sh", "-c", scriptFile)
|
cmd := exec.Command(scriptLauncher[0], append(scriptLauncher[1:], scriptFile)...)
|
||||||
cmd.Stdin = c.App.Reader
|
cmd.Stdin = c.App.Reader
|
||||||
cmd.Stdout = c.App.Writer
|
cmd.Stdout = c.App.Writer
|
||||||
cmd.Stderr = c.App.ErrWriter
|
cmd.Stderr = c.App.ErrWriter
|
||||||
@@ -214,15 +223,6 @@ func runCommandInternal(c *cli.Context, command string, m *client.Message) error
|
|||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTmpScript(command string) (string, error) {
|
|
||||||
scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
|
|
||||||
script := fmt.Sprintf("#!/bin/sh\n%s", command)
|
|
||||||
if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return scriptFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func envVars(m *client.Message) []string {
|
func envVars(m *client.Message) []string {
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
|
env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
|
||||||
@@ -249,13 +249,26 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
|
|||||||
if filename != "" {
|
if filename != "" {
|
||||||
return client.LoadConfig(filename)
|
return client.LoadConfig(filename)
|
||||||
}
|
}
|
||||||
u, _ := user.Current()
|
configFile := defaultConfigFile()
|
||||||
configFile := defaultClientRootConfigFile
|
|
||||||
if u.Uid != "0" {
|
|
||||||
configFile = util.ExpandHome(defaultClientUserConfigFile)
|
|
||||||
}
|
|
||||||
if s, _ := os.Stat(configFile); s != nil {
|
if s, _ := os.Stat(configFile); s != nil {
|
||||||
return client.LoadConfig(configFile)
|
return client.LoadConfig(configFile)
|
||||||
}
|
}
|
||||||
return client.NewConfig(), nil
|
return client.NewConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 Conditionally used in different builds
|
||||||
|
func defaultConfigFileUnix() string {
|
||||||
|
u, _ := user.Current()
|
||||||
|
configFile := clientRootConfigFileUnixAbsolute
|
||||||
|
if u.Uid != "0" {
|
||||||
|
homeDir, _ := os.UserConfigDir()
|
||||||
|
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
|
||||||
|
}
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
|
|
||||||
|
//lint:ignore U1000 Conditionally used in different builds
|
||||||
|
func defaultConfigFileWindows() string {
|
||||||
|
homeDir, _ := os.UserConfigDir()
|
||||||
|
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
|
||||||
|
}
|
||||||
|
|||||||
16
cmd/subscribe_darwin.go
Normal file
16
cmd/subscribe_darwin.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
const (
|
||||||
|
scriptExt = "sh"
|
||||||
|
scriptHeader = "#!/bin/sh\n"
|
||||||
|
clientCommandDescriptionSuffix = `The default config file for all client commands is /etc/ntfy/client.yml (if root user),
|
||||||
|
or "~/Library/Application Support/ntfy/client.yml" for all other users.`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultConfigFile() string {
|
||||||
|
return defaultConfigFileUnix()
|
||||||
|
}
|
||||||
16
cmd/subscribe_linux.go
Normal file
16
cmd/subscribe_linux.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
const (
|
||||||
|
scriptExt = "sh"
|
||||||
|
scriptHeader = "#!/bin/sh\n"
|
||||||
|
clientCommandDescriptionSuffix = `The default config file for all client commands is /etc/ntfy/client.yml (if root user),
|
||||||
|
or ~/.config/ntfy/client.yml for all other users.`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultConfigFile() string {
|
||||||
|
return defaultConfigFileUnix()
|
||||||
|
}
|
||||||
15
cmd/subscribe_windows.go
Normal file
15
cmd/subscribe_windows.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
const (
|
||||||
|
scriptExt = "bat"
|
||||||
|
scriptHeader = ""
|
||||||
|
clientCommandDescriptionSuffix = `The default config file for all client commands is %AppData%\ntfy\client.yml.`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultConfigFile() string {
|
||||||
|
return defaultConfigFileWindows()
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !noserver
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,13 +13,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commands = append(commands, cmdUser)
|
||||||
|
}
|
||||||
|
|
||||||
var flagsUser = userCommandFlags()
|
var flagsUser = userCommandFlags()
|
||||||
|
|
||||||
var cmdUser = &cli.Command{
|
var cmdUser = &cli.Command{
|
||||||
Name: "user",
|
Name: "user",
|
||||||
Usage: "Manage/show users",
|
Usage: "Manage/show users",
|
||||||
UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...",
|
UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...",
|
||||||
Flags: flagsUser,
|
Flags: flagsUser,
|
||||||
Before: initConfigFileInputSource("config", flagsUser),
|
Before: initConfigFileInputSourceFunc("config", flagsUser),
|
||||||
Category: categoryServer,
|
Category: categoryServer,
|
||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
{
|
{
|
||||||
|
|||||||
112
docs/config.md
112
docs/config.md
@@ -227,7 +227,7 @@ The easiest way to configure a private instance is to set `auth-default-access`
|
|||||||
|
|
||||||
=== "/etc/ntfy/server.yml"
|
=== "/etc/ntfy/server.yml"
|
||||||
``` yaml
|
``` yaml
|
||||||
auth-file "/var/lib/ntfy/user.db"
|
auth-file: "/var/lib/ntfy/user.db"
|
||||||
auth-default-access: "deny-all"
|
auth-default-access: "deny-all"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -618,6 +618,35 @@ Example:
|
|||||||
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
|
firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## iOS instant notifications
|
||||||
|
Unlike Android, iOS heavily restricts background processing, which sadly makes it impossible to implement instant
|
||||||
|
push notifications without a central server.
|
||||||
|
|
||||||
|
To still support instant notifications on iOS through your self-hosted ntfy server, you have to forward so called `poll_request`
|
||||||
|
messages to the main ntfy.sh server (or any upstream server that's APNS/Firebase connected, if you build your own iOS app),
|
||||||
|
which will then forward it to Firebase/APNS.
|
||||||
|
|
||||||
|
To configure it, simply set `upstream-base-url` like so:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
upstream-base-url: "https://ntfy.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
If set, all incoming messages will publish a poll request to the configured upstream server, containing
|
||||||
|
the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
|
||||||
|
|
||||||
|
If `upstream-base-url` is not set, notifications will still eventually get to your device, but delivery can take hours,
|
||||||
|
depending on the state of th phone. If you are using your phone, it shouldn't take more than 20-30 minutes though.
|
||||||
|
|
||||||
|
In case you're curious, here's an example of the entire flow:
|
||||||
|
|
||||||
|
- In the iOS app, you subscribe to `https://ntfy.example.com/mytopic`
|
||||||
|
- The app subscribes to the Firebase topic `6de73be8dfb7d69e...` (the SHA256 of the topic URL)
|
||||||
|
- When you publish a message to `https://ntfy.example.com/mytopic`, your ntfy server will publish a
|
||||||
|
poll request to `https://ntfy.sh/6de73be8dfb7d69e...` (passing the message ID in the `X-Poll-ID` header)
|
||||||
|
- The ntfy.sh server publishes the message to Firebase, which forwards it to APNS, which forwards it to your iOS device
|
||||||
|
- Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -775,6 +804,11 @@ Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `l
|
|||||||
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
|
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
|
||||||
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
All config options can also be defined in the `server.yml` file using underscores instead of dashes, e.g.
|
||||||
|
`cache_duration` and `cache-duration` are both supported. This is to support stricter YAML parsers that do
|
||||||
|
not support dashes.
|
||||||
|
|
||||||
| Config option | Env variable | Format | Default | Description |
|
| Config option | Env variable | Format | Default | Description |
|
||||||
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
|
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
|
||||||
@@ -802,7 +836,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||||
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
|
| `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. |
|
||||||
| `web-root` | `NTFY_WEB_ROOT` | `app` or `home` | `app` | Sets web root to landing page (home) or web app (app) |
|
| `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) |
|
||||||
| `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. |
|
||||||
| `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-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`. |
|
||||||
@@ -839,42 +873,42 @@ DESCRIPTION:
|
|||||||
ntfy serve --listen-http :8080 # Starts server with alternate port
|
ntfy serve --listen-http :8080 # Starts server with alternate port
|
||||||
|
|
||||||
OPTIONS:
|
OPTIONS:
|
||||||
--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, -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, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
--listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
||||||
--listen-https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
--listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
||||||
--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]
|
||||||
--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]
|
||||||
--cert-file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
|
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
|
||||||
--firebase-key-file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
|
--firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
|
||||||
--cache-file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
||||||
--cache-duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
||||||
--auth-file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
|
--auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
|
||||||
--auth-default-access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
|
--auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
|
||||||
--attachment-cache-dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
|
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
|
||||||
--attachment-total-size-limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||||
--attachment-file-size-limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
|
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
|
||||||
--attachment-expiry-duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
|
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
|
||||||
--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, -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]
|
||||||
--web-root value sets web root to landing page (home) or web app (app) (default: "app") [$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]
|
||||||
--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 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 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]
|
||||||
--smtp-sender-from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
|
--smtp-sender-from value, --smtp_sender_from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_FROM]
|
||||||
--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 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 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]
|
||||||
--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 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 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]
|
||||||
--visitor-attachment-daily-bandwidth-limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
|
--visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
|
||||||
--visitor-request-limit-burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
|
--visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
|
||||||
--visitor-request-limit-replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
||||||
--visitor-request-limit-exempt-hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
|
--visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
|
||||||
--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 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]
|
||||||
--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]
|
||||||
--help, -h show help (default: false)
|
--help, -h show help (default: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -112,15 +112,15 @@ by typing `make`:
|
|||||||
$ make
|
$ make
|
||||||
Typical commands (more see below):
|
Typical commands (more see below):
|
||||||
make build - Build web app, documentation and server/client (sloowwww)
|
make build - Build web app, documentation and server/client (sloowwww)
|
||||||
make server-amd64 - Build server/client binary (amd64, no web app or docs)
|
make cli-linux-amd64 - Build server/client binary (amd64, no web app or docs)
|
||||||
make install-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)
|
make install-linux-amd64 - Install ntfy binary to /usr/bin/ntfy (amd64)
|
||||||
make web - Build the web app
|
make web - Build the web app
|
||||||
make docs - Build the documentation
|
make docs - Build the documentation
|
||||||
make check - Run all tests, vetting/formatting checks and linters
|
make check - Run all tests, vetting/formatting checks and linters
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to build the **ntfy binary including web app and docs for all supported architectures** (amd64, armv7, and amd64),
|
If you want to build the **ntfy binary including web app and docs for all supported architectures** (amd64, armv7, and arm64),
|
||||||
you can simply run `make build`:
|
you can simply run `make build`:
|
||||||
|
|
||||||
``` shell
|
``` shell
|
||||||
@@ -158,47 +158,52 @@ $ 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 the ntfy binary
|
### Build the ntfy binary
|
||||||
To build only the `ntfy` binary **without the web app or documentation**, use the `make server-...` targets:
|
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
|
||||||
|
|
||||||
``` shell
|
``` shell
|
||||||
$ make
|
$ make
|
||||||
Build server & client (not release version):
|
Build server & client (using GoReleaser, not release version):
|
||||||
make server - Build server & client (all architectures)
|
make cli - Build server & client (all architectures)
|
||||||
make server-amd64 - Build server & client (amd64 only)
|
make cli-linux-amd64 - Build server & client (Linux, amd64 only)
|
||||||
make server-armv7 - Build server & client (armv7 only)
|
make cli-linux-armv6 - Build server & client (Linux, armv6 only)
|
||||||
make server-arm64 - Build server & client (arm64 only)
|
make cli-linux-armv7 - Build server & client (Linux, armv7 only)
|
||||||
|
make cli-linux-arm64 - Build server & client (Linux, arm64 only)
|
||||||
|
make cli-windows-amd64 - Build client (Windows, amd64 only)
|
||||||
|
make cli-darwin-all - Build client (macOS, arm64+amd64 universal binary)
|
||||||
```
|
```
|
||||||
|
|
||||||
So if you're on an amd64/x86_64-based machine, you may just want to run `make server-amd64` during testing. On a modern
|
So if you're on an amd64/x86_64-based machine, you may just want to run `make cli-linux-amd64` during testing. On a modern
|
||||||
system, this shouldn't take longer than 5-10 seconds. I often combine it with `install-amd64` so I can run the binary
|
system, this shouldn't take longer than 5-10 seconds. I often combine it with `install-linux-amd64` so I can run the binary
|
||||||
right away:
|
right away:
|
||||||
|
|
||||||
``` shell
|
``` shell
|
||||||
$ make server-amd64 install-amd64
|
$ make cli-linux-amd64 install-linux-amd64
|
||||||
$ ntfy serve
|
$ ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
**During development of the main app, you can also just use `go run main.go`**, as long as you run
|
**During development of the main app, you can also just use `go run main.go`**, as long as you run
|
||||||
`make server-deps-static-sites`at least once and `CGO_ENABLED=1`:
|
`make cli-deps-static-sites`at least once and `CGO_ENABLED=1`:
|
||||||
|
|
||||||
``` shell
|
``` shell
|
||||||
$ export CGO_ENABLED=1
|
$ export CGO_ENABLED=1
|
||||||
$ make server-deps-static-sites
|
$ make cli-deps-static-sites
|
||||||
$ go run main.go serve
|
$ go run main.go serve
|
||||||
2022/03/18 08:43:55 Listening on :2586[http]
|
2022/03/18 08:43:55 Listening on :2586[http]
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't run `server-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
|
If you don't run `cli-deps-static-sites`, you may see an error *`pattern ...: no matching files found`*:
|
||||||
```
|
```
|
||||||
$ go run main.go serve
|
$ go run main.go serve
|
||||||
server/server.go:85:13: pattern docs: no matching files found
|
server/server.go:85:13: pattern docs: no matching files found
|
||||||
```
|
```
|
||||||
|
|
||||||
This is because we use `go:embed` to embed the documentation and web app, so the Go code expects files to be
|
This is because we use `go:embed` to embed the documentation and web app, so the Go code expects files to be
|
||||||
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `server-deps-static-sites`
|
present at `server/docs` and `server/site`. If they are not, you'll see the above error. The `cli-deps-static-sites`
|
||||||
target creates dummy files that ensures that you'll be able to build.
|
target creates dummy files that ensure that you'll be able to build.
|
||||||
|
|
||||||
|
While not officially supported (or released), you can build and run the server **on macOS** as well. Simply run
|
||||||
|
`make cli-darwin-server` to build a binary, or `go run main.go serve` (see above) to run it.
|
||||||
|
|
||||||
### Build the web app
|
### Build the web app
|
||||||
The sources for the web app live in `web/`. As long as you have `npm` installed (see above), building the web app
|
The sources for the web app live in `web/`. As long as you have `npm` installed (see above), building the web app
|
||||||
@@ -210,7 +215,7 @@ $ make web
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will build the web app using Create React App and then **copy the production build to the `server/site` folder**, so
|
This will build the web app using Create React App and then **copy the production build to the `server/site` folder**, so
|
||||||
that when you `make server` (or `make server-amd64`, ...), you will have the web app included in the `ntfy` binary.
|
that when you `make cli` (or `make cli-linux-amd64`, ...), you will have the web app included in the `ntfy` binary.
|
||||||
|
|
||||||
If you're developing on the web app, it's best to just `cd web` and run `npm start` manually. This will open your browser
|
If you're developing on the web app, it's best to just `cd web` and run `npm start` manually. This will open your browser
|
||||||
at `http://127.0.0.1:3000` with the web app, and as you edit the source files, they will be recompiled and the browser
|
at `http://127.0.0.1:3000` with the web app, and as you edit the source files, they will be recompiled and the browser
|
||||||
@@ -282,9 +287,13 @@ Then either follow the steps for building with or without Firebase.
|
|||||||
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
|
I do build the ntfy Android app using IntelliJ IDEA (Android Studio), so I don't know if these Gradle commands will
|
||||||
work without issues. Please give me feedback if it does/doesn't work for you.
|
work without issues. Please give me feedback if it does/doesn't work for you.
|
||||||
|
|
||||||
Without Firebase, you may want to still change the default `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
|
Without Firebase, you may want to still change the default `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
|
||||||
if you're self-hosting the server. Then run:
|
if you're self-hosting the server. Then run:
|
||||||
```
|
```
|
||||||
|
# Remove Google dependencies (FCM)
|
||||||
|
sed -i -e '/google-services/d' build.gradle
|
||||||
|
sed -i -e '/google-services/d' app/build.gradle
|
||||||
|
|
||||||
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
|
# To build an unsigned .apk (app/build/outputs/apk/fdroid/*.apk)
|
||||||
./gradlew assembleFdroidRelease
|
./gradlew assembleFdroidRelease
|
||||||
|
|
||||||
@@ -301,7 +310,7 @@ To build your own version with Firebase, you must:
|
|||||||
|
|
||||||
* Create a Firebase/FCM account
|
* Create a Firebase/FCM account
|
||||||
* Place your account file at `app/google-services.json`
|
* Place your account file at `app/google-services.json`
|
||||||
* And change `app_base_url` in [strings.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/strings.xml)
|
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
|
||||||
* Then run:
|
* Then run:
|
||||||
```
|
```
|
||||||
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
|
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
|
||||||
@@ -310,3 +319,9 @@ To build your own version with Firebase, you must:
|
|||||||
# To build a bundle .aab (app/play/release/*.aab)
|
# To build a bundle .aab (app/play/release/*.aab)
|
||||||
./gradlew bundlePlayRelease
|
./gradlew bundlePlayRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## iOS app
|
||||||
|
The ntfy iOS app source code is available [on GitHub](https://github.com/binwiederhier/ntfy-ios).
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
I haven't had time to move the build instructions here. Please check out the repository instead.
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ There are a million ways to use ntfy, but here are some inspirations. I try to c
|
|||||||
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
|
<a href="https://github.com/binwiederhier/ntfy/tree/main/examples">examples on GitHub</a>, so be sure to check
|
||||||
those out, too.
|
those out, too.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Many of these examples were contributed by ntfy users. If you have other examples of how you use ntfy, please
|
||||||
|
[create a pull request](https://github.com/binwiederhier/ntfy/pulls), and I'll happily include it. Also note, that
|
||||||
|
I cannot guarantee that all of these examples are functional. Many of them I have not tried myself.
|
||||||
|
|
||||||
## A long process is done: backups, copying data, pipelines, ...
|
## A long process is done: backups, copying data, pipelines, ...
|
||||||
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
|
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
|
||||||
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>
|
||||||
@@ -98,7 +103,8 @@ One of my co-workers uses the following Ansible task to let him know when things
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Watchtower notifications (shoutrrr)
|
## Watchtower notifications (shoutrrr)
|
||||||
You can use `shoutrrr` generic webhook support to send watchtower notifications to your ntfy topic.
|
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
||||||
|
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||||
|
|
||||||
Example docker-compose.yml:
|
Example docker-compose.yml:
|
||||||
```yml
|
```yml
|
||||||
@@ -122,7 +128,6 @@ GitHub have been hopeless. In case it ever becomes available, I want to know imm
|
|||||||
``` cron
|
``` 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
|
||||||
~
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
|
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
|
||||||
@@ -132,11 +137,10 @@ Some simple bash scripts to achieve this are kindly provided in [nickexyz's repo
|
|||||||
## 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:
|
||||||
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example: Send a message (click to expand)</summary>
|
<summary>Example: Send a message (click to expand)</summary>
|
||||||
|
|
||||||
```
|
``` json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "c956e688cc74ad8e",
|
"id": "c956e688cc74ad8e",
|
||||||
@@ -225,7 +229,7 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
|
|||||||
<details>
|
<details>
|
||||||
<summary>Example: Send a picture (click to expand)</summary>
|
<summary>Example: Send a picture (click to expand)</summary>
|
||||||
|
|
||||||
```
|
``` json
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"id": "d135a13eadeb9d6d",
|
"id": "d135a13eadeb9d6d",
|
||||||
@@ -341,8 +345,8 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
|
|||||||
|
|
||||||
## Gatus service health check
|
## Gatus service health check
|
||||||
|
|
||||||
An example for a custom alert with <a href="https://github.com/TwiN/gatus">Gatus</a>
|
An example for a custom alert with [Gatus](https://github.com/TwiN/gatus):
|
||||||
```
|
``` yaml
|
||||||
alerting:
|
alerting:
|
||||||
custom:
|
custom:
|
||||||
url: "https://ntfy.sh"
|
url: "https://ntfy.sh"
|
||||||
@@ -366,3 +370,18 @@ alerting:
|
|||||||
TRIGGERED: "warning"
|
TRIGGERED: "warning"
|
||||||
RESOLVED: "white_check_mark"
|
RESOLVED: "white_check_mark"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Jellyseerr/Overseerr webhook
|
||||||
|
Here is an example for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) webhook
|
||||||
|
JSON payload. Remember to change the `https://requests.example.com` to your jellyseerr/overseerr URL.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"topic": "requests",
|
||||||
|
"title": "{{event}}",
|
||||||
|
"message": "{{subject}}\n{{message}}\n\nRequested by: {{requestedBy_username}}\n\nStatus: {{media_status}}\nRequest Id: {{request_id}}",
|
||||||
|
"priority": 4,
|
||||||
|
"attach": "{{image}}",
|
||||||
|
"click": "https://requests.example.com/{{media_type}}/{{media_tmdbid}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ or POST requests. I use it to notify myself when scripts fail, or long-running c
|
|||||||
## Step 1: Get the app
|
## Step 1: Get the app
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
|
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
|
||||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
|
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
|
||||||
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>
|
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
|
||||||
|
|
||||||
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
|
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
|
||||||
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
|
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
|
||||||
@@ -83,7 +83,7 @@ This will create a notification that looks like this:
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
That's it. You're all set. Go play and read the rest of the docs. I highly recommend reading at least the page on
|
That's it. You're all set. Go play and read the rest of the docs. I highly recommend reading at least the page on
|
||||||
[publishing messages](publish.md), as well as the detailed page on the [Android app](subscribe/phone.md).
|
[publishing messages](publish.md), as well as the detailed page on the [Android/iOS app](subscribe/phone.md).
|
||||||
|
|
||||||
Here's another video showing the entire process:
|
Here's another video showing the entire process:
|
||||||
|
|
||||||
|
|||||||
@@ -13,50 +13,50 @@ The ntfy server comes as a statically linked binary and is shipped as tarball, d
|
|||||||
We support amd64, armv7 and arm64.
|
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 (see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
|
||||||
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (or `/etc/ntfy/client.yml`, see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (or `/etc/ntfy/client.yml`, see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
|
||||||
|
|
||||||
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
To 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).
|
||||||
|
|
||||||
## Binaries and packages
|
## 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/v1.21.2/ntfy_1.21.2_linux_x86_64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_x86_64.tar.gz
|
||||||
tar zxvf ntfy_1.21.2_linux_x86_64.tar.gz
|
tar zxvf ntfy_1.24.0_linux_x86_64.tar.gz
|
||||||
sudo cp -a ntfy_1.21.2_linux_x86_64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.24.0_linux_x86_64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_1.21.2_linux_armv6.tar.gz
|
tar zxvf ntfy_1.24.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_1.21.2_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.24.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_1.21.2_linux_armv7.tar.gz
|
tar zxvf ntfy_1.24.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_1.21.2_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.24.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_1.21.2_linux_arm64.tar.gz
|
tar zxvf ntfy_1.24.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_1.21.2_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_1.24.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.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
|
||||||
@@ -111,7 +111,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.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
|
||||||
@@ -119,7 +119,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.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
|
||||||
@@ -127,7 +127,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.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
|
||||||
@@ -137,28 +137,28 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_amd64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
@@ -176,6 +176,39 @@ cd ntfysh-bin
|
|||||||
makepkg -si
|
makepkg -si
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
||||||
|
To install, please download the tarball, extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||||
|
|
||||||
|
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
||||||
|
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_v1.24.0_macOS_all.tar.gz > ntfy_v1.24.0_macOS_all.tar.gz
|
||||||
|
tar zxvf ntfy_v1.24.0_macOS_all.tar.gz
|
||||||
|
sudo cp -a ntfy_v1.24.0_macOS_all/ntfy /usr/local/bin/ntfy
|
||||||
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
|
cp ntfy_v1.24.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
|
ntfy --help
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
If there is a desire to install ntfy via [Homebrew](https://brew.sh/), please create a
|
||||||
|
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know. Also, you can build and run the
|
||||||
|
ntfy server on macOS as well, though I don't officially support that. Check out the [build instructions](develop.md)
|
||||||
|
for details.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
||||||
|
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_v1.24.0_windows_x86_64.zip),
|
||||||
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
|
|
||||||
|
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a
|
||||||
|
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
||||||
be pretty straight forward to use.
|
be pretty straight forward to use.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Here's an example showing how to publish a simple message using a POST request:
|
|||||||
|
|
||||||
=== "PowerShell"
|
=== "PowerShell"
|
||||||
``` powershell
|
``` powershell
|
||||||
Invoke-RestMethod -Method 'Post' -Uri https://ntfy.sh/topic -Body "Backup successful 😀" -UseBasicParsing
|
Invoke-RestMethod -Method 'Post' -Uri https://ntfy.sh/mytopic -Body "Backup successful" -UseBasicParsing
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Python"
|
=== "Python"
|
||||||
@@ -296,6 +296,8 @@ an [external image attachment](#attach-file-from-a-url) and [email publishing](#
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Message title
|
## Message title
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
The notification title is typically set to the topic short URL (e.g. `ntfy.sh/mytopic`). To override the title,
|
The notification title is typically set to the topic short URL (e.g. `ntfy.sh/mytopic`). To override the title,
|
||||||
you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
|
you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
|
||||||
|
|
||||||
@@ -372,7 +374,9 @@ you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Message priority
|
## Message priority
|
||||||
All messages have a priority, which defines how urgently your phone notifies you. You can set custom
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
|
All messages have a priority, which defines how urgently your phone notifies you. On Android, you can set custom
|
||||||
notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](subscribe/phone.md)).
|
notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](subscribe/phone.md)).
|
||||||
|
|
||||||
The following priorities exist:
|
The following priorities exist:
|
||||||
@@ -460,6 +464,8 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Tags & emojis 🥳 🎉
|
## Tags & emojis 🥳 🎉
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can tag messages with emojis and other relevant strings:
|
You can tag messages with emojis and other relevant strings:
|
||||||
|
|
||||||
* **Emojis**: If a tag matches an [emoji short code](emojis.md), it'll be converted to an emoji and prepended
|
* **Emojis**: If a tag matches an [emoji short code](emojis.md), it'll be converted to an emoji and prepended
|
||||||
@@ -579,6 +585,8 @@ them with a comma, e.g. `tag1,tag2,tag3`.
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Scheduled delivery
|
## Scheduled delivery
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can delay the delivery of messages and let ntfy send them at a later date. This can be used to send yourself
|
You can delay the delivery of messages and let ntfy send them at a later date. This can be used to send yourself
|
||||||
reminders or even to execute commands at a later date (if your subscriber acts on messages).
|
reminders or even to execute commands at a later date (if your subscriber acts on messages).
|
||||||
|
|
||||||
@@ -679,6 +687,8 @@ Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Tim
|
|||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
## Webhooks (publish via GET)
|
## Webhooks (publish via GET)
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
In addition to using PUT/POST, you can also send to topics via simple HTTP GET requests. This makes it easy to use
|
In addition to using PUT/POST, you can also send to topics via simple HTTP GET requests. This makes it easy to use
|
||||||
a ntfy topic as a [webhook](https://en.wikipedia.org/wiki/Webhook), or if your client has limited HTTP support (e.g.
|
a ntfy topic as a [webhook](https://en.wikipedia.org/wiki/Webhook), or if your client has limited HTTP support (e.g.
|
||||||
like the [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) Android app).
|
like the [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) Android app).
|
||||||
@@ -782,6 +792,8 @@ Here's an example with a custom message, tags and a priority:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Publish as JSON
|
## Publish as JSON
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
For some integrations with other tools (e.g. [Jellyfin](https://jellyfin.org/), [overseerr](https://overseerr.dev/)),
|
For some integrations with other tools (e.g. [Jellyfin](https://jellyfin.org/), [overseerr](https://overseerr.dev/)),
|
||||||
adding custom headers to HTTP requests may be tricky or impossible, so ntfy also allows publishing the entire message
|
adding custom headers to HTTP requests may be tricky or impossible, so ntfy also allows publishing the entire message
|
||||||
as JSON in the request body.
|
as JSON in the request body.
|
||||||
@@ -943,6 +955,8 @@ all the supported fields:
|
|||||||
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
|
| `email` | - | *e-mail address* | `phil@example.com` | E-mail address for e-mail notifications |
|
||||||
|
|
||||||
## Action buttons
|
## Action buttons
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can add action buttons to notifications to allow yourself to react to a notification directly. This is incredibly
|
You can add action buttons to notifications to allow yourself to react to a notification directly. This is incredibly
|
||||||
useful and has countless applications.
|
useful and has countless applications.
|
||||||
|
|
||||||
@@ -953,7 +967,7 @@ As of today, the following actions are supported:
|
|||||||
|
|
||||||
* [`view`](#open-websiteapp): Opens a website or app when the action button is tapped
|
* [`view`](#open-websiteapp): Opens a website or app when the action button is tapped
|
||||||
* [`broadcast`](#send-android-broadcast): Sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
* [`broadcast`](#send-android-broadcast): Sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
||||||
when the action button is tapped
|
when the action button is tapped (only supported on Android)
|
||||||
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
|
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
|
||||||
|
|
||||||
Here's an example of what that a notification with actions can look like:
|
Here's an example of what that a notification with actions can look like:
|
||||||
@@ -1276,6 +1290,8 @@ The required/optional fields for each action depend on the type of the action it
|
|||||||
for details.
|
for details.
|
||||||
|
|
||||||
### Open website/app
|
### Open website/app
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
The `view` action **opens a website or app when the action button is tapped**, e.g. a browser, a Google Maps location, or
|
The `view` action **opens a website or app when the action button is tapped**, e.g. a browser, a Google Maps location, or
|
||||||
even a deep link into Twitter or a show ntfy topic. How exactly the action is handled depends on how Android and your
|
even a deep link into Twitter or a show ntfy topic. How exactly the action is handled depends on how Android and your
|
||||||
desktop browser treat the links. Normally it'll just open a link in the browser.
|
desktop browser treat the links. Normally it'll just open a link in the browser.
|
||||||
@@ -1515,6 +1531,8 @@ The `view` action supports the following fields:
|
|||||||
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after action button is tapped |
|
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after action button is tapped |
|
||||||
|
|
||||||
### Send Android broadcast
|
### Send Android broadcast
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
The `broadcast` action **sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
The `broadcast` action **sends an [Android broadcast](https://developer.android.com/guide/components/broadcasts) intent
|
||||||
when the action button is tapped**. This allows integration into automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
|
when the action button is tapped**. This allows integration into automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
|
||||||
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm), which basically means
|
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm), which basically means
|
||||||
@@ -1779,6 +1797,8 @@ The `broadcast` action supports the following fields:
|
|||||||
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after action button is tapped |
|
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after action button is tapped |
|
||||||
|
|
||||||
### Send HTTP request
|
### Send HTTP request
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
The `http` action **sends a HTTP request when the action button is tapped**. You can use this to trigger REST APIs
|
The `http` action **sends a HTTP request when the action button is tapped**. You can use this to trigger REST APIs
|
||||||
for whatever systems you have, e.g. opening the garage door, or turning on/off lights.
|
for whatever systems you have, e.g. opening the garage door, or turning on/off lights.
|
||||||
|
|
||||||
@@ -1791,14 +1811,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
```
|
```
|
||||||
curl \
|
curl \
|
||||||
-d "Garage door has been open for 15 minutes. Close it?" \
|
-d "Garage door has been open for 15 minutes. Close it?" \
|
||||||
-H "Actions: http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
|
-H "Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
|
||||||
ntfy.sh/myhome
|
ntfy.sh/myhome
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "ntfy CLI"
|
=== "ntfy CLI"
|
||||||
```
|
```
|
||||||
ntfy publish \
|
ntfy publish \
|
||||||
--actions="http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
|
--actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" \
|
||||||
myhome \
|
myhome \
|
||||||
"Garage door has been open for 15 minutes. Close it?"
|
"Garage door has been open for 15 minutes. Close it?"
|
||||||
```
|
```
|
||||||
@@ -1807,7 +1827,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
``` http
|
``` http
|
||||||
POST /myhome HTTP/1.1
|
POST /myhome HTTP/1.1
|
||||||
Host: ntfy.sh
|
Host: ntfy.sh
|
||||||
Actions: http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={"action": "close"}
|
Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={"action": "close"}
|
||||||
|
|
||||||
Garage door has been open for 15 minutes. Close it?
|
Garage door has been open for 15 minutes. Close it?
|
||||||
```
|
```
|
||||||
@@ -1818,7 +1838,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: 'Garage door has been open for 15 minutes. Close it?',
|
body: 'Garage door has been open for 15 minutes. Close it?',
|
||||||
headers: {
|
headers: {
|
||||||
'Actions': 'http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}'
|
'Actions': 'http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
@@ -1826,14 +1846,14 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
=== "Go"
|
=== "Go"
|
||||||
``` go
|
``` go
|
||||||
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Garage door has been open for 15 minutes. Close it?"))
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/myhome", strings.NewReader("Garage door has been open for 15 minutes. Close it?"))
|
||||||
req.Header.Set("Actions", "http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}")
|
req.Header.Set("Actions", "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}")
|
||||||
http.DefaultClient.Do(req)
|
http.DefaultClient.Do(req)
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "PowerShell"
|
=== "PowerShell"
|
||||||
``` powershell
|
``` powershell
|
||||||
$uri = "https://ntfy.sh/myhome"
|
$uri = "https://ntfy.sh/myhome"
|
||||||
$headers = @{ Actions="http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" }
|
$headers = @{ Actions="http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" }
|
||||||
$body = "Garage door has been open for 15 minutes. Close it?"
|
$body = "Garage door has been open for 15 minutes. Close it?"
|
||||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
Invoke-RestMethod -Method 'Post' -Uri $uri -Headers $headers -Body $body -UseBasicParsing
|
||||||
```
|
```
|
||||||
@@ -1842,7 +1862,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
``` python
|
``` python
|
||||||
requests.post("https://ntfy.sh/myhome",
|
requests.post("https://ntfy.sh/myhome",
|
||||||
data="Garage door has been open for 15 minutes. Close it?",
|
data="Garage door has been open for 15 minutes. Close it?",
|
||||||
headers={ "Actions": "http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" })
|
headers={ "Actions": "http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}" })
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "PHP"
|
=== "PHP"
|
||||||
@@ -1852,7 +1872,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
|||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' =>
|
'header' =>
|
||||||
"Content-Type: text/plain\r\n" .
|
"Content-Type: text/plain\r\n" .
|
||||||
"Actions: http, Cloor door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
|
"Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
|
||||||
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
||||||
]
|
]
|
||||||
]));
|
]));
|
||||||
@@ -2055,6 +2075,8 @@ The `http` action supports the following fields:
|
|||||||
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared. |
|
| `clear` | -️ | *boolean* | `false` | `true` | Clear notification after HTTP request succeeds. If the request fails, the notification is not cleared. |
|
||||||
|
|
||||||
## Click action
|
## Click action
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can define which URL to open when a notification is clicked. This may be useful if your notification is related
|
You can define which URL to open when a notification is clicked. This may be useful if your notification is related
|
||||||
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
|
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
|
||||||
the web browser (or the app) and open the website.
|
the web browser (or the app) and open the website.
|
||||||
@@ -2143,6 +2165,8 @@ Here's an example that will open Reddit when the notification is clicked:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Attachments
|
## Attachments
|
||||||
|
_Supported on:_ :material-android: :material-firefox:
|
||||||
|
|
||||||
You can **send images and other files to your phone** as attachments to a notification. The attachments are then downloaded
|
You can **send images and other files to your phone** as attachments to a notification. The attachments are then downloaded
|
||||||
onto your phone (depending on size and setting automatically), and can be used from the Downloads folder.
|
onto your phone (depending on size and setting automatically), and can be used from the Downloads folder.
|
||||||
|
|
||||||
@@ -2315,6 +2339,8 @@ Here's an example showing how to attach an APK file:
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## E-mail notifications
|
## E-mail notifications
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that
|
You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that
|
||||||
you'd like to persist longer, or to blast-notify yourself on all possible channels.
|
you'd like to persist longer, or to blast-notify yourself on all possible channels.
|
||||||
|
|
||||||
@@ -2425,6 +2451,8 @@ Here's what that looks like in Google Mail:
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## E-mail publishing
|
## E-mail publishing
|
||||||
|
_Supported on:_ :material-android: :material-apple: :material-firefox:
|
||||||
|
|
||||||
You can publish messages to a topic via e-mail, i.e. by sending an email to a specific address. For instance, you can
|
You can publish messages to a topic via e-mail, i.e. by sending an email to a specific address. For instance, you can
|
||||||
publish a message to the topic `sometopic` by sending an e-mail to `ntfy-sometopic@ntfy.sh`. This is useful for e-mail
|
publish a message to the topic `sometopic` by sending an e-mail to `ntfy-sometopic@ntfy.sh`. This is useful for e-mail
|
||||||
based integrations such as for statuspage.io (though these days most services also support webhooks and HTTP calls).
|
based integrations such as for statuspage.io (though these days most services also support webhooks and HTTP calls).
|
||||||
@@ -2504,9 +2532,11 @@ Here's a simple example:
|
|||||||
=== "PowerShell"
|
=== "PowerShell"
|
||||||
``` powershell
|
``` powershell
|
||||||
$uri = "https://ntfy.example.com/mysecrets"
|
$uri = "https://ntfy.example.com/mysecrets"
|
||||||
$headers = @{ Authorization="Basic cGhpbDpteXBhc3M=" }
|
$credentials = 'username:password'
|
||||||
$body = "Look ma, with auth"
|
$encodedCredentials = [convert]::ToBase64String([text.Encoding]::UTF8.GetBytes($credentials))
|
||||||
Invoke-RestMethod -Method 'Post' -Uri $uri -Body $body -Headers $headers -UseBasicParsing
|
$headers = @{Authorization="Basic $encodedCredentials"}
|
||||||
|
$message = "Look ma, with auth"
|
||||||
|
Invoke-RestMethod -Uri $uri -Body $message -Headers $headers -Method "Post" -UseBasicParsing
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "Python"
|
=== "Python"
|
||||||
@@ -2752,4 +2782,5 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
|
|||||||
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
||||||
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
||||||
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |
|
| `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps |
|
||||||
|
| `X-Poll-ID` | `Poll-ID` | Internal parameter, used for [iOS push notifications](config.md#ios-instant-notifications) |
|
||||||
| `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics |
|
| `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics |
|
||||||
|
|||||||
123
docs/releases.md
123
docs/releases.md
@@ -4,11 +4,106 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
## ntfy Android app v1.13.0 (UNRELEASED)
|
## ntfy iOS app v1.1 (UNRELEASED)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/224), thanks to [@cmeis](https://github.com/cmeis) for reporting)
|
* [Message priority](https://ntfy.sh/docs/publish/#message-priority) support (no ticket)
|
||||||
|
* [Tags/emojis](https://ntfy.sh/docs/publish/#tags-emojis) support (no ticket)
|
||||||
|
* [Action buttons](https://ntfy.sh/docs/publish/#action-buttons) support (no ticket)
|
||||||
|
* [Click action](https://ntfy.sh/docs/publish/#click-action) support (no ticket)
|
||||||
|
* Open topic when notification clicked (no ticket)
|
||||||
|
* Notification now makes a sound and vibrates (no ticket)
|
||||||
|
* Cancel notifications when navigating to topic (no ticket)
|
||||||
|
* iOS 14.0 support (no ticket, [PR#1](https://github.com/binwiederhier/ntfy-ios/pull/1), thanks to [@callum-99](https://github.com/callum-99))
|
||||||
|
|
||||||
|
**Bugs:**
|
||||||
|
|
||||||
|
* iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267))
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
## ntfy server v1.24.0
|
||||||
|
Released May 28, 2022
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Regularly send Firebase keepalive messages to ~poll topic to support self-hosted servers (no ticket)
|
||||||
|
* Add subscribe filter to query exact messages by ID (no ticket)
|
||||||
|
* Support for `poll_request` messages to support [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for self-hosted servers (no ticket)
|
||||||
|
|
||||||
|
**Bugs:**
|
||||||
|
|
||||||
|
* Support emails without `Content-Type` ([#265](https://github.com/binwiederhier/ntfy/issues/265), thanks to [@dmbonsall](https://github.com/dmbonsall))
|
||||||
|
|
||||||
|
**Additional translations:**
|
||||||
|
|
||||||
|
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
|
||||||
|
|
||||||
|
## ntfy Android app v1.14.0 (UNRELEASED)
|
||||||
|
|
||||||
|
**Additional translations:**
|
||||||
|
|
||||||
|
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
|
||||||
|
|
||||||
|
## ntfy iOS app v1.0
|
||||||
|
Released May 25, 2022
|
||||||
|
|
||||||
|
This is the first version of the ntfy iOS app. It supports only ntfy.sh (no selfhosted servers) and only messages + title
|
||||||
|
(no priority, tags, attachments, ...). I'll rapidly add (hopefully) most of the other ntfy features, and then I'll focus
|
||||||
|
on self-hosted servers.
|
||||||
|
|
||||||
|
The app is now available in the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||||
|
|
||||||
|
**Tickets:**
|
||||||
|
|
||||||
|
* iOS app ([#4](https://github.com/binwiederhier/ntfy/issues/4), see also: [TestFlight summary](https://github.com/binwiederhier/ntfy/issues/4#issuecomment-1133767150))
|
||||||
|
|
||||||
|
**Thanks:**
|
||||||
|
|
||||||
|
* Thank you to all the testers who tried out the app. You guys gave me the confidence that it's ready to release (albeit with
|
||||||
|
some known issues which will be addressed in follow-up releases).
|
||||||
|
|
||||||
|
## ntfy server v1.23.0
|
||||||
|
Released May 21, 2022
|
||||||
|
|
||||||
|
This release ships a CLI for Windows and macOS, as well as the ability to disable the web app entirely. On top of that,
|
||||||
|
it adds support for APNs, the iOS messaging service. This is needed for the (soon to be released) iOS app.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* [Windows](https://ntfy.sh/docs/install/#windows) and [macOS](https://ntfy.sh/docs/install/#macos) builds for the [ntfy CLI](https://ntfy.sh/docs/subscribe/cli/) ([#112](https://github.com/binwiederhier/ntfy/issues/112))
|
||||||
|
* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
|
||||||
|
* Add APNs config to Firebase messages to support [iOS app](https://github.com/binwiederhier/ntfy/issues/4) ([#247](https://github.com/binwiederhier/ntfy/pull/247), thanks to [@Copephobia](https://github.com/Copephobia))
|
||||||
|
|
||||||
|
**Bugs:**
|
||||||
|
|
||||||
|
* Support underscores in server.yml config options ([#255](https://github.com/binwiederhier/ntfy/issues/255), thanks to [@ajdelgado](https://github.com/ajdelgado))
|
||||||
|
* Force MAKEFLAGS to --jobs=1 in `Makefile` ([#257](https://github.com/binwiederhier/ntfy/pull/257), thanks to [@oddlama](https://github.com/oddlama))
|
||||||
|
|
||||||
|
**Documentation:**
|
||||||
|
|
||||||
|
* Typo in install instructions ([#252](https://github.com/binwiederhier/ntfy/pull/252)/[#251](https://github.com/binwiederhier/ntfy/issues/251), thanks to [@oddlama](https://github.com/oddlama))
|
||||||
|
* Fix typo in private server example ([#262](https://github.com/binwiederhier/ntfy/pull/262), thanks to [@MayeulC](https://github.com/MayeulC))
|
||||||
|
* [Examples](examples.md) for [jellyseerr](https://github.com/Fallenbagel/jellyseerr)/[overseerr](https://overseerr.dev/) ([#264](https://github.com/binwiederhier/ntfy/pull/264), thanks to [@Fallenbagel](https://github.com/Fallenbagel))
|
||||||
|
|
||||||
|
**Additional translations:**
|
||||||
|
|
||||||
|
* Portuguese/Brazil (thanks to [@tiagotriques](https://hosted.weblate.org/user/tiagotriques/) and [@pireshenrique22](https://hosted.weblate.org/user/pireshenrique22/))
|
||||||
|
|
||||||
|
Thank you to the many translators, who helped translate the new strings so quickly. I am humbled and amazed by your help.
|
||||||
|
|
||||||
|
## ntfy Android app v1.13.0
|
||||||
|
Released May 11, 2022
|
||||||
|
|
||||||
|
This release brings a slightly altered design for the detail view, featuring a card layout to make notifications more easily
|
||||||
|
distinguishable from one another. It also ships per-topic settings that allow overriding minimum priority, auto delete threshold
|
||||||
|
and custom icons. Aside from that, we've got tons of bug fixes as usual.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Per-subscription settings, custom subscription icons ([#155](https://github.com/binwiederhier/ntfy/issues/155), thanks to [@mztiq](https://github.com/mztiq) for reporting)
|
||||||
|
* Cards in notification detail view ([#175](https://github.com/binwiederhier/ntfy/issues/175), thanks to [@cmeis](https://github.com/cmeis) for reporting)
|
||||||
|
|
||||||
**Bugs:**
|
**Bugs:**
|
||||||
|
|
||||||
@@ -18,18 +113,30 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
* Fix app icon on old Android versions ([#128](https://github.com/binwiederhier/ntfy/issues/128), thanks to [@shadow00](https://github.com/shadow00) for reporting)
|
* Fix app icon on old Android versions ([#128](https://github.com/binwiederhier/ntfy/issues/128), thanks to [@shadow00](https://github.com/shadow00) for reporting)
|
||||||
* Fix races in UnifiedPush registration ([#230](https://github.com/binwiederhier/ntfy/issues/230), thanks to @Jakob for reporting)
|
* Fix races in UnifiedPush registration ([#230](https://github.com/binwiederhier/ntfy/issues/230), thanks to @Jakob for reporting)
|
||||||
* Prevent view action from crashing the app ([#233](https://github.com/binwiederhier/ntfy/issues/233))
|
* Prevent view action from crashing the app ([#233](https://github.com/binwiederhier/ntfy/issues/233))
|
||||||
|
* Prevent long topic names and icons from overlapping ([#240](https://github.com/binwiederhier/ntfy/issues/240), thanks to [@cmeis](https://github.com/cmeis) for reporting)
|
||||||
|
|
||||||
**Thanks for testing:**
|
**Additional translations:**
|
||||||
|
|
||||||
|
* Dutch (*incomplete*, thanks to [@diony](https://hosted.weblate.org/user/diony/))
|
||||||
|
|
||||||
|
**Thank you:**
|
||||||
|
|
||||||
Thanks to [@cmeis](https://github.com/cmeis), [@StoyanDimitrov](https://github.com/StoyanDimitrov), [@Fallenbagel](https://github.com/Fallenbagel) for testing, and
|
Thanks to [@cmeis](https://github.com/cmeis), [@StoyanDimitrov](https://github.com/StoyanDimitrov), [@Fallenbagel](https://github.com/Fallenbagel) for testing, and
|
||||||
to [@Joeharrison94](https://github.com/Joeharrison94) for the input.
|
to [@Joeharrison94](https://github.com/Joeharrison94) for the input. And thank you very much to all the translators for catching up so quickly.
|
||||||
|
|
||||||
## ntfy server v1.22.0 (UNRELEASED)
|
## ntfy server v1.22.0
|
||||||
|
Released May 7, 2022
|
||||||
|
|
||||||
|
This release makes the web app more accessible to people with disabilities, and introduces a "mark as read" icon in the web app.
|
||||||
|
It also fixes a curious bug with WebSockets and Apache and makes the notification sounds in the web app a little quieter.
|
||||||
|
|
||||||
|
We've also improved the documentation a little and added translations for three more languages.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
* Better parsing of the user actions, allowing quotes (no ticket)
|
|
||||||
* Make web app more accessible ([#217](https://github.com/binwiederhier/ntfy/issues/217))
|
* Make web app more accessible ([#217](https://github.com/binwiederhier/ntfy/issues/217))
|
||||||
|
* Better parsing of the user actions, allowing quotes (no ticket)
|
||||||
|
* Add "mark as read" icon button to notification ([#243](https://github.com/binwiederhier/ntfy/pull/243), thanks to [@wunter8](https://github.com/wunter8))
|
||||||
|
|
||||||
**Bugs:**
|
**Bugs:**
|
||||||
|
|
||||||
@@ -41,18 +148,18 @@ to [@Joeharrison94](https://github.com/Joeharrison94) for the input.
|
|||||||
|
|
||||||
* Improved caddy configuration (no ticket, thanks to @Stnby)
|
* Improved caddy configuration (no ticket, thanks to @Stnby)
|
||||||
* Additional multi-line examples on the [publish page](https://ntfy.sh/docs/publish/) ([#234](https://github.com/binwiederhier/ntfy/pull/234), thanks to [@aTable](https://github.com/aTable))
|
* Additional multi-line examples on the [publish page](https://ntfy.sh/docs/publish/) ([#234](https://github.com/binwiederhier/ntfy/pull/234), thanks to [@aTable](https://github.com/aTable))
|
||||||
|
* Fixed PowerShell auth example to use UTF-8 ([#242](https://github.com/binwiederhier/ntfy/pull/242), thanks to [@SMAW](https://github.com/SMAW))
|
||||||
|
|
||||||
**Additional translations:**
|
**Additional translations:**
|
||||||
|
|
||||||
* Czech (thanks to [@waclaw66](https://hosted.weblate.org/user/waclaw66/))
|
* Czech (thanks to [@waclaw66](https://hosted.weblate.org/user/waclaw66/))
|
||||||
* French (thanks to [@nathanaelhoun](https://hosted.weblate.org/user/nathanaelhoun/))
|
* French (thanks to [@nathanaelhoun](https://hosted.weblate.org/user/nathanaelhoun/))
|
||||||
|
* Hungarian (thanks to [@agocsdaniel](https://hosted.weblate.org/user/agocsdaniel/))
|
||||||
|
|
||||||
**Thanks for testing:**
|
**Thanks for testing:**
|
||||||
|
|
||||||
Thanks to [@wunter8](https://github.com/wunter8) for testing.
|
Thanks to [@wunter8](https://github.com/wunter8) for testing.
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
## ntfy Android app v1.12.0
|
## ntfy Android app v1.12.0
|
||||||
Released Apr 25, 2022
|
Released Apr 25, 2022
|
||||||
|
|
||||||
|
|||||||
4
docs/static/css/extra.css
vendored
4
docs/static/css/extra.css
vendored
@@ -8,8 +8,8 @@
|
|||||||
width: unset !important;
|
width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-sidebar {
|
.md-header__topic:first-child {
|
||||||
width: 12.5rem !important;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-typeset h4 {
|
.md-typeset h4 {
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ curl -s "ntfy.sh/mytopic/json?poll=1&sched=1"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Filter messages
|
### Filter messages
|
||||||
You can filter which messages are returned based on the well-known message fields `message`, `title`, `priority` and
|
You can filter which messages are returned based on the well-known message fields `id`, `message`, `title`, `priority` and
|
||||||
`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags
|
`tags`. Here's an example that only returns messages of high or urgent priority that contains the both tags
|
||||||
"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND.
|
"zfs-error" and "error". Note that the `priority` filter is a logical OR and the `tags` filter is a logical AND.
|
||||||
|
|
||||||
@@ -280,12 +280,13 @@ $ curl "ntfy.sh/alerts/json?priority=high&tags=zfs-error"
|
|||||||
|
|
||||||
Available filters (all case-insensitive):
|
Available filters (all case-insensitive):
|
||||||
|
|
||||||
| Filter variable | Alias | Example | Description |
|
| Filter variable | Alias | Example | Description |
|
||||||
|-----------------|---------------------------|------------------------------------|-------------------------------------------------------------------------|
|
|-----------------|---------------------------|-----------------------------------------------|-------------------------------------------------------------------------|
|
||||||
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic?message=lalala` | Only return messages that match this exact message string |
|
| `id` | `X-ID` | `ntfy.sh/mytopic/json?poll=1&id=pbkiz8SD7ZxG` | Only return messages that match this exact message ID |
|
||||||
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic?title=some+title` | Only return messages that match this exact title string |
|
| `message` | `X-Message`, `m` | `ntfy.sh/mytopic/json?message=lalala` | Only return messages that match this exact message string |
|
||||||
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
|
| `title` | `X-Title`, `t` | `ntfy.sh/mytopic/json?title=some+title` | Only return messages that match this exact title string |
|
||||||
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?tags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
|
| `priority` | `X-Priority`, `prio`, `p` | `ntfy.sh/mytopic/json?p=high,urgent` | Only return messages that match *any priority listed* (comma-separated) |
|
||||||
|
| `tags` | `X-Tags`, `tag`, `ta` | `ntfy.sh/mytopic?/jsontags=error,alert` | Only return messages that match *all listed tags* (comma-separated) |
|
||||||
|
|
||||||
### Subscribe to multiple topics
|
### Subscribe to multiple topics
|
||||||
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
|
It's possible to subscribe to multiple topics in one HTTP call by providing a comma-separated list of topics
|
||||||
@@ -315,18 +316,19 @@ format of the message. It's very straight forward:
|
|||||||
|
|
||||||
**Message**:
|
**Message**:
|
||||||
|
|
||||||
| Field | Required | Type | Example | Description |
|
| Field | Required | Type | Example | Description |
|
||||||
|--------------|----------|---------------------------------------------------|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------|----------|---------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `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 |
|
||||||
| `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 |
|
||||||
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
|
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); if not set defaults to `ntfy.sh/<topic>` |
|
||||||
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
|
| `tags` | - | *string array* | `["tag1","tag2"]` | List of [tags](../publish.md#tags-emojis) that may or not map to emojis |
|
||||||
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
| `priority` | - | *1, 2, 3, 4, or 5* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
||||||
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
|
| `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](../publish.md#click-action) |
|
||||||
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
|
| `actions` | - | *JSON array* | *see [actions buttons](../publish.md#action-buttons)* | [Action buttons](../publish.md#action-buttons) that can be displayed in the notification |
|
||||||
|
| `attachment` | - | *JSON object* | *see below* | Details about an attachment (name, URL, size, ...) |
|
||||||
|
|
||||||
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
|
**Attachment** (part of the message, see [attachments](../publish.md#attachments) for details):
|
||||||
|
|
||||||
@@ -416,6 +418,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
|
|||||||
| `poll` | `X-Poll`, `po` | Return cached messages and close connection |
|
| `poll` | `X-Poll`, `po` | Return cached messages and close connection |
|
||||||
| `since` | `X-Since`, `si` | Return cached messages since timestamp, duration or message ID |
|
| `since` | `X-Since`, `si` | Return cached messages since timestamp, duration or message ID |
|
||||||
| `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list |
|
| `scheduled` | `X-Scheduled`, `sched` | Include scheduled/delayed messages in message list |
|
||||||
|
| `id` | `X-ID` | Filter: Only return messages that match this exact message ID |
|
||||||
| `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string |
|
| `message` | `X-Message`, `m` | Filter: Only return messages that match this exact message string |
|
||||||
| `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string |
|
| `title` | `X-Title`, `t` | Filter: Only return messages that match this exact title string |
|
||||||
| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) |
|
| `priority` | `X-Priority`, `prio`, `p` | Filter: Only return messages that match *any priority listed* (comma-separated) |
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ which will read the `subscribe` config from the config file. Please also check o
|
|||||||
|
|
||||||
Here's an example config file that subscribes to three different topics, executing a different command for each of them:
|
Here's an example config file that subscribes to three different topics, executing a different command for each of them:
|
||||||
|
|
||||||
=== "~/.config/ntfy/client.yml"
|
=== "~/.config/ntfy/client.yml (Linux)"
|
||||||
```yaml
|
```yaml
|
||||||
subscribe:
|
subscribe:
|
||||||
- topic: echo-this
|
- topic: echo-this
|
||||||
@@ -145,12 +145,42 @@ Here's an example config file that subscribes to three different topics, executi
|
|||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
=== "~/Library/Application Support/ntfy/client.yml (macOS)"
|
||||||
|
```yaml
|
||||||
|
subscribe:
|
||||||
|
- topic: echo-this
|
||||||
|
command: 'echo "Message received: $message"'
|
||||||
|
- topic: alerts
|
||||||
|
command: osascript -e "display notification \"$message\""
|
||||||
|
if:
|
||||||
|
priority: high,urgent
|
||||||
|
- topic: calc
|
||||||
|
command: open -a Calculator
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "%AppData%\ntfy\client.yml (Windows)"
|
||||||
|
```yaml
|
||||||
|
subscribe:
|
||||||
|
- topic: echo-this
|
||||||
|
command: 'echo Message received: %message%'
|
||||||
|
- topic: alerts
|
||||||
|
command: |
|
||||||
|
notifu /m "%NTFY_MESSAGE%"
|
||||||
|
exit 0
|
||||||
|
if:
|
||||||
|
priority: high,urgent
|
||||||
|
- topic: calc
|
||||||
|
command: calc
|
||||||
|
```
|
||||||
|
|
||||||
In this example, when `ntfy subscribe --from-config` is executed:
|
In this example, when `ntfy subscribe --from-config` is executed:
|
||||||
|
|
||||||
* Messages to `echo-this` simply echos to standard out
|
* Messages to `echo-this` simply echos to standard out
|
||||||
* Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html)
|
* Messages to `alerts` display as desktop notification for high priority messages using [notify-send](https://manpages.ubuntu.com/manpages/focal/man1/notify-send.1.html) (Linux),
|
||||||
* Messages to `calc` open the gnome calculator 😀 (*because, why not*)
|
[notifu](https://www.paralint.com/projects/notifu/) (Windows) or `osascript` (macOS)
|
||||||
* Messages to `print-temp` execute an inline script and print the CPU temperature
|
* Messages to `calc` open the calculator 😀 (*because, why not*)
|
||||||
|
* Messages to `print-temp` execute an inline script and print the CPU temperature (Linux version only)
|
||||||
|
|
||||||
I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:
|
I hope this shows how powerful this command is. Here's a short video that demonstrates the above example:
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# Subscribe from your phone
|
# Subscribe from your phone
|
||||||
You can use the [ntfy Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) to receive
|
You can use the ntfy [Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [iOS app](https://apps.apple.com/us/app/ntfy/id1625396347)
|
||||||
notifications directly on your phone. Just like the server, this app is also [open source](https://github.com/binwiederhier/ntfy-android).
|
to receive notifications directly on your phone. Just like the server, this app is also open source, and the code is available
|
||||||
Since I don't have an iPhone or a Mac, I didn't make an iOS app yet. I'd be awesome if [someone else could help out](https://github.com/binwiederhier/ntfy/issues/4).
|
on GitHub ([Android](https://github.com/binwiederhier/ntfy-android), [iOS](https://github.com/binwiederhier/ntfy-ios)). Feel free to
|
||||||
|
contribute, or [build your own](../develop.md).
|
||||||
|
|
||||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
|
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
|
||||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
|
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
|
||||||
|
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
|
||||||
|
|
||||||
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
|
You can get the Android app from both [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
|
||||||
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 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).
|
||||||
|
|
||||||
## 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
|
||||||
@@ -31,6 +33,8 @@ If those screenshots are still not enough, here's a video:
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Message priority
|
## Message priority
|
||||||
|
_Supported on:_ :material-android: :material-apple:
|
||||||
|
|
||||||
When you [publish messages](../publish.md#message-priority) to a topic, you can **define a priority**. This priority defines
|
When you [publish messages](../publish.md#message-priority) to a topic, you can **define a priority**. This priority defines
|
||||||
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.
|
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.
|
||||||
|
|
||||||
@@ -59,6 +63,8 @@ setting, and other settings such as popover or notification dot:
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
## Instant delivery
|
## Instant delivery
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
Instant delivery allows you to receive messages on your phone instantly, **even when your phone is in doze mode**, i.e.
|
Instant delivery allows you to receive messages on your phone instantly, **even when your phone is in doze mode**, i.e.
|
||||||
when the screen turns off, and you leave it on the desk for a while. This is achieved with a foreground service, which
|
when the screen turns off, and you leave it on the desk for a while. This is achieved with a foreground service, which
|
||||||
you'll see as a permanent notification that looks like this:
|
you'll see as a permanent notification that looks like this:
|
||||||
@@ -89,6 +95,8 @@ The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in
|
|||||||
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.
|
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.
|
||||||
|
|
||||||
## Share to topic
|
## Share to topic
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
|
You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
|
||||||
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
|
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
|
||||||
you shared content to and lists them at the bottom.
|
you shared content to and lists them at the bottom.
|
||||||
@@ -101,6 +109,8 @@ The feature is pretty self-explanatory, and one picture says more than a thousan
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
## ntfy:// links
|
## ntfy:// links
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
The ntfy Android app supports deep linking directly to topics. This is useful when integrating with [automation apps](#automation-apps)
|
The ntfy Android app supports deep linking directly to topics. This is useful when integrating with [automation apps](#automation-apps)
|
||||||
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
|
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
|
||||||
or to simply directly link to a topic from a mobile website.
|
or to simply directly link to a topic from a mobile website.
|
||||||
@@ -119,6 +129,8 @@ or to simply directly link to a topic from a mobile website.
|
|||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
### UnifiedPush
|
### UnifiedPush
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
[UnifiedPush](https://unifiedpush.org) is a standard for receiving push notifications without using the Google-owned
|
[UnifiedPush](https://unifiedpush.org) is a standard for receiving push notifications without using the Google-owned
|
||||||
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) service. It puts push notifications
|
[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) service. It puts push notifications
|
||||||
in the control of the user. ntfy can act as a **UnifiedPush distributor**, forwarding messages to apps that support it.
|
in the control of the user. ntfy can act as a **UnifiedPush distributor**, forwarding messages to apps that support it.
|
||||||
@@ -134,6 +146,8 @@ to handle messages. Here's an example with [FluffyChat](https://fluffychat.im/):
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Automation apps
|
### Automation apps
|
||||||
|
_Supported on:_ :material-android:
|
||||||
|
|
||||||
The ntfy Android app integrates nicely with automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
|
The ntfy Android app integrates nicely with automation apps such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
|
||||||
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm). Using Android intents, you can
|
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm). Using Android intents, you can
|
||||||
**react to incoming messages**, as well as **send messages**.
|
**react to incoming messages**, as well as **send messages**.
|
||||||
@@ -210,9 +224,3 @@ The following intent extras are supported when for the intent with the `io.hecke
|
|||||||
| `message` ❤️ | ✔ | *String* | `Some message` | Message body; **you must set this** |
|
| `message` ❤️ | ✔ | *String* | `Some message` | Message body; **you must set this** |
|
||||||
| `tags` | - | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
|
| `tags` | - | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
|
||||||
| `priority` | - | *String or Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
| `priority` | - | *String or Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
|
||||||
|
|
||||||
## iPhone/iOS
|
|
||||||
I almost feel devious for putting the *Download on the App Store* button on this page. Currently, there is no iOS app
|
|
||||||
for ntfy, but it's in the works. You can track the status on GitHub.
|
|
||||||
|
|
||||||
<a href="https://github.com/binwiederhier/ntfy/issues/4"><img src="../../static/img/badge-appstore.png"></a>
|
|
||||||
|
|||||||
36
go.mod
36
go.mod
@@ -4,30 +4,30 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.6.1 // indirect
|
cloud.google.com/go/firestore v1.6.1 // indirect
|
||||||
cloud.google.com/go/storage v1.22.0 // indirect
|
cloud.google.com/go/storage v1.22.1 // indirect
|
||||||
firebase.google.com/go v3.13.0+incompatible
|
firebase.google.com/go v3.13.0+incompatible
|
||||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/emersion/go-smtp v0.15.0
|
github.com/emersion/go-smtp v0.15.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0
|
github.com/gabriel-vasile/mimetype v1.4.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.12
|
github.com/mattn/go-sqlite3 v1.14.13
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/urfave/cli/v2 v2.4.7
|
github.com/urfave/cli/v2 v2.8.1
|
||||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
|
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 // indirect
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29
|
||||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
|
||||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
||||||
google.golang.org/api v0.75.0
|
google.golang.org/api v0.81.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/pkg/errors v0.9.1 // indirect
|
require github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.101.0 // indirect
|
cloud.google.com/go v0.102.0 // indirect
|
||||||
cloud.google.com/go/compute v1.6.1 // indirect
|
cloud.google.com/go/compute v1.6.1 // indirect
|
||||||
cloud.google.com/go/iam v0.3.0 // indirect
|
cloud.google.com/go/iam v0.3.0 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
@@ -35,19 +35,21 @@ require (
|
|||||||
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
|
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.7 // indirect
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
|
||||||
github.com/googleapis/go-type-adapters v1.0.0 // indirect
|
github.com/googleapis/go-type-adapters v1.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.opencensus.io v0.23.0 // indirect
|
go.opencensus.io v0.23.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
|
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
|
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 // indirect
|
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
|
||||||
google.golang.org/grpc v1.46.0 // indirect
|
google.golang.org/grpc v1.46.2 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
google.golang.org/protobuf v1.28.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
79
go.sum
79
go.sum
@@ -27,8 +27,8 @@ cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW
|
|||||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||||
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
|
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
|
||||||
cloud.google.com/go v0.101.0 h1:g+LL+JvpvdyGtcaD2xw2mSByE/6F9s471eJSoaysM84=
|
cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8=
|
||||||
cloud.google.com/go v0.101.0/go.mod h1:hEiddgDb77jDQ+I80tURYNJEnuwPzFU8awCFFRLKjW0=
|
cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
@@ -56,8 +56,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
|||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8=
|
cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg=
|
||||||
cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE=
|
cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
|
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
|
||||||
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
|
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
|
||||||
@@ -86,8 +86,9 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
|
|||||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.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=
|
||||||
@@ -160,15 +161,15 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
|
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||||
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
|
||||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
|
||||||
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
@@ -185,13 +186,16 @@ github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLe
|
|||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
|
||||||
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
|
||||||
github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
|
||||||
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
|
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
|
||||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
@@ -209,8 +213,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
|
||||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
|
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 h1:oDSPaYiL2dbjcArLrFS8ANtwgJMyOLzvQCZon+XmFsk=
|
||||||
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -231,8 +235,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/urfave/cli/v2 v2.4.7 h1:nUgKLTC/InVYwUx26HZUBGIBZaptiW97W8vVlhuYawo=
|
github.com/urfave/cli/v2 v2.8.1 h1:CGuYNZF9IKZY/rfBe3lJpccSoIY1ytfvmgQT90cNOl4=
|
||||||
github.com/urfave/cli/v2 v2.4.7/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
|
github.com/urfave/cli/v2 v2.8.1/go.mod h1:Z41J9TPoffeoqP0Iza0YbAhGvymRdZAd2uPmZ5JxRdY=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -252,8 +258,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
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/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -330,8 +336,10 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
|
|||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8=
|
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
|
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||||
|
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -351,8 +359,9 @@ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ
|
|||||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -363,8 +372,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||||
|
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -420,12 +430,14 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
|
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
|
||||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -496,8 +508,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
|
|
||||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
|
||||||
|
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
@@ -534,8 +547,11 @@ google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQ
|
|||||||
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
|
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
|
||||||
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
|
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
|
||||||
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
|
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
|
||||||
google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg=
|
|
||||||
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
|
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
|
||||||
|
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||||
|
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||||
|
google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
|
||||||
|
google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
|
||||||
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
@@ -614,13 +630,17 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2
|
|||||||
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||||
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
|
||||||
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
|
||||||
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 h1:nquqdM9+ps0JZcIiI70+tqoaIFS5Ql4ZuK8UXnz3HfE=
|
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
||||||
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
|
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||||
|
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||||
|
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||||
|
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||||
|
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
|
||||||
|
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||||
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
@@ -649,8 +669,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
|
|||||||
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||||
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||||
google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
|
|
||||||
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||||
|
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
||||||
|
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||||
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=
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ markdown_extensions:
|
|||||||
custom_checkbox: true
|
custom_checkbox: true
|
||||||
- attr_list
|
- attr_list
|
||||||
- md_in_html
|
- md_in_html
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ const (
|
|||||||
DefaultAtSenderInterval = 10 * time.Second
|
DefaultAtSenderInterval = 10 * time.Second
|
||||||
DefaultMinDelay = 10 * time.Second
|
DefaultMinDelay = 10 * time.Second
|
||||||
DefaultMaxDelay = 3 * 24 * time.Hour
|
DefaultMaxDelay = 3 * 24 * time.Hour
|
||||||
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
|
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery
|
||||||
|
DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all global and per-visitor limits
|
// Defines all global and per-visitor limits
|
||||||
@@ -67,6 +68,8 @@ type Config struct {
|
|||||||
WebRootIsApp bool
|
WebRootIsApp bool
|
||||||
AtSenderInterval time.Duration
|
AtSenderInterval time.Duration
|
||||||
FirebaseKeepaliveInterval time.Duration
|
FirebaseKeepaliveInterval time.Duration
|
||||||
|
FirebasePollInterval time.Duration
|
||||||
|
UpstreamBaseURL string
|
||||||
SMTPSenderAddr string
|
SMTPSenderAddr string
|
||||||
SMTPSenderUser string
|
SMTPSenderUser string
|
||||||
SMTPSenderPass string
|
SMTPSenderPass string
|
||||||
@@ -88,6 +91,7 @@ type Config struct {
|
|||||||
VisitorEmailLimitBurst int
|
VisitorEmailLimitBurst int
|
||||||
VisitorEmailLimitReplenish time.Duration
|
VisitorEmailLimitReplenish time.Duration
|
||||||
BehindProxy bool
|
BehindProxy bool
|
||||||
|
EnableWeb bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig instantiates a default new server config
|
// NewConfig instantiates a default new server config
|
||||||
@@ -116,6 +120,7 @@ func NewConfig() *Config {
|
|||||||
MaxDelay: DefaultMaxDelay,
|
MaxDelay: DefaultMaxDelay,
|
||||||
AtSenderInterval: DefaultAtSenderInterval,
|
AtSenderInterval: DefaultAtSenderInterval,
|
||||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||||
|
FirebasePollInterval: DefaultFirebasePollInterval,
|
||||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||||
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
||||||
@@ -126,5 +131,6 @@ func NewConfig() *Config {
|
|||||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||||
BehindProxy: false,
|
BehindProxy: false,
|
||||||
|
EnableWeb: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
145
server/server.go
145
server/server.go
@@ -3,16 +3,12 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"heckel.io/ntfy/auth"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -28,6 +24,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"heckel.io/ntfy/auth"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is the main server, providing the UI and API for ntfy
|
// Server is the main server, providing the UI and API for ntfy
|
||||||
@@ -90,7 +92,9 @@ var (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
firebaseControlTopic = "~control" // See Android if changed
|
firebaseControlTopic = "~control" // See Android if changed
|
||||||
|
firebasePollTopic = "~poll" // See iOS if changed
|
||||||
emptyMessageBody = "triggered" // Used if message body is empty
|
emptyMessageBody = "triggered" // Used if message body is empty
|
||||||
|
newMessageBody = "New message" // Used in poll requests as generic message
|
||||||
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
||||||
encodingBase64 = "base64"
|
encodingBase64 = "base64"
|
||||||
)
|
)
|
||||||
@@ -263,23 +267,23 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
||||||
return s.handleHome(w, r)
|
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
|
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
|
||||||
return s.handleExample(w, r)
|
return s.ensureWebEnabled(s.handleExample)(w, r, v)
|
||||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||||
return s.handleEmpty(w, r, v)
|
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||||
return s.handleWebConfig(w, r)
|
return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
||||||
return s.handleUserStats(w, r, v)
|
return s.handleUserStats(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||||
return s.handleStatic(w, r)
|
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||||
return s.handleDocs(w, r)
|
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
|
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
|
||||||
return s.limitRequests(s.handleFile)(w, r, v)
|
return s.limitRequests(s.handleFile)(w, r, v)
|
||||||
} else if r.Method == http.MethodOptions {
|
} else if r.Method == http.MethodOptions {
|
||||||
return s.handleOptions(w, r)
|
return s.ensureWebEnabled(s.handleOptions)(w, r, v)
|
||||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
|
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
|
||||||
return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
|
return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
|
||||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
||||||
@@ -297,21 +301,21 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
|
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
|
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
|
||||||
return s.handleTopic(w, r)
|
return s.ensureWebEnabled(s.handleTopic)(w, r, v)
|
||||||
}
|
}
|
||||||
return errHTTPNotFound
|
return errHTTPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if s.config.WebRootIsApp {
|
if s.config.WebRootIsApp {
|
||||||
r.URL.Path = webAppIndex
|
r.URL.Path = webAppIndex
|
||||||
} else {
|
} else {
|
||||||
r.URL.Path = webHomeIndex
|
r.URL.Path = webHomeIndex
|
||||||
}
|
}
|
||||||
return s.handleStatic(w, r)
|
return s.handleStatic(w, r, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see PUT/POST too!
|
unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see PUT/POST too!
|
||||||
if unifiedpush {
|
if unifiedpush {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -320,7 +324,7 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.URL.Path = webAppIndex
|
r.URL.Path = webAppIndex
|
||||||
return s.handleStatic(w, r)
|
return s.handleStatic(w, r, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
@@ -334,12 +338,12 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
|
func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
_, err := io.WriteString(w, exampleSource)
|
_, err := io.WriteString(w, exampleSource)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleWebConfig(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
appRoot := "/"
|
appRoot := "/"
|
||||||
if !s.config.WebRootIsApp {
|
if !s.config.WebRootIsApp {
|
||||||
appRoot = "/app"
|
appRoot = "/app"
|
||||||
@@ -367,13 +371,13 @@ func (s *Server) handleUserStats(w http.ResponseWriter, r *http.Request, v *visi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
r.URL.Path = webSiteDir + r.URL.Path
|
r.URL.Path = webSiteDir + r.URL.Path
|
||||||
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
|
util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
||||||
util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
|
util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -420,6 +424,9 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if m.PollID != "" {
|
||||||
|
m = newPollRequestMessage(t.ID, m.PollID)
|
||||||
|
}
|
||||||
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
|
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -433,18 +440,13 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.firebase != nil && firebase && !delayed {
|
if s.firebase != nil && firebase && !delayed {
|
||||||
go func() {
|
go s.sendToFirebase(v, m)
|
||||||
if err := s.firebase(m); err != nil {
|
|
||||||
log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
if s.mailer != nil && email != "" && !delayed {
|
if s.mailer != nil && email != "" && !delayed {
|
||||||
go func() {
|
go s.sendEmail(v, m, email)
|
||||||
if err := s.mailer.Send(v.ip, email, m); err != nil {
|
}
|
||||||
log.Printf("[%s] MAIL - Unable to send email: %v", v.ip, err.Error())
|
if s.config.UpstreamBaseURL != "" {
|
||||||
}
|
go s.forwardPollRequest(v, m)
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
if cache {
|
if cache {
|
||||||
if err := s.messageCache.AddMessage(m); err != nil {
|
if err := s.messageCache.AddMessage(m); err != nil {
|
||||||
@@ -462,6 +464,38 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendToFirebase(v *visitor, m *message) {
|
||||||
|
if err := s.firebase(m); err != nil {
|
||||||
|
log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) sendEmail(v *visitor, m *message, email string) {
|
||||||
|
if err := s.mailer.Send(v.ip, email, m); err != nil {
|
||||||
|
log.Printf("[%s] MAIL - Unable to send email: %v", v.ip, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||||
|
topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
|
||||||
|
topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL)))
|
||||||
|
forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash)
|
||||||
|
req, err := http.NewRequest("POST", forwardURL, strings.NewReader(""))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("X-Poll-ID", m.ID)
|
||||||
|
response, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error())
|
||||||
|
return
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
log.Printf("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
|
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
|
||||||
cache = readBoolParam(r, true, "x-cache", "cache")
|
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||||
@@ -547,32 +581,42 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
|
|||||||
firebase = false
|
firebase = false
|
||||||
unifiedpush = true
|
unifiedpush = true
|
||||||
}
|
}
|
||||||
|
m.PollID = readParam(r, "x-poll-id", "poll-id")
|
||||||
|
if m.PollID != "" {
|
||||||
|
unifiedpush = false
|
||||||
|
cache = false
|
||||||
|
email = ""
|
||||||
|
}
|
||||||
return cache, firebase, email, unifiedpush, nil
|
return cache, firebase, email, unifiedpush, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
||||||
//
|
//
|
||||||
// 1. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
||||||
|
// If a message is flagged as poll request, the body does not matter and is discarded
|
||||||
|
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
||||||
// If body is binary, encode as base64, if not do not encode
|
// If body is binary, encode as base64, if not do not encode
|
||||||
// 2. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
||||||
// Body must be a message, because we attached an external URL
|
// Body must be a message, because we attached an external URL
|
||||||
// 3. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
||||||
// Body must be attachment, because we passed a filename
|
// Body must be attachment, because we passed a filename
|
||||||
// 4. curl -T file.txt ntfy.sh/mytopic
|
|
||||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
|
||||||
// 5. curl -T file.txt ntfy.sh/mytopic
|
// 5. curl -T file.txt ntfy.sh/mytopic
|
||||||
|
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||||
|
// 6. curl -T file.txt ntfy.sh/mytopic
|
||||||
// If file.txt is > message limit, treat it as an attachment
|
// If file.txt is > message limit, treat it as an attachment
|
||||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
|
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
|
||||||
if unifiedpush {
|
if m.Event == pollRequestEvent { // Case 1
|
||||||
return s.handleBodyAsMessageAutoDetect(m, body) // Case 1
|
return nil
|
||||||
|
} else if unifiedpush {
|
||||||
|
return s.handleBodyAsMessageAutoDetect(m, body) // Case 2
|
||||||
} else if m.Attachment != nil && m.Attachment.URL != "" {
|
} else if m.Attachment != nil && m.Attachment.URL != "" {
|
||||||
return s.handleBodyAsTextMessage(m, body) // Case 2
|
return s.handleBodyAsTextMessage(m, body) // Case 3
|
||||||
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
||||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 3
|
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
||||||
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
||||||
return s.handleBodyAsTextMessage(m, body) // Case 4
|
return s.handleBodyAsTextMessage(m, body) // Case 5
|
||||||
}
|
}
|
||||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 5
|
return s.handleBodyAsAttachment(r, v, m, body) // Case 6
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedReadCloser) error {
|
func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedReadCloser) error {
|
||||||
@@ -904,7 +948,7 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
|
|||||||
return sinceNoMessages, errHTTPBadRequestSinceInvalid
|
return sinceNoMessages, errHTTPBadRequestSinceInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
|
func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
|
w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
|
||||||
@@ -1073,7 +1117,11 @@ func (s *Server) runFirebaseKeepaliver() {
|
|||||||
select {
|
select {
|
||||||
case <-time.After(s.config.FirebaseKeepaliveInterval):
|
case <-time.After(s.config.FirebaseKeepaliveInterval):
|
||||||
if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil {
|
if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil {
|
||||||
log.Printf("error sending Firebase keepalive message: %s", err.Error())
|
log.Printf("error sending Firebase keepalive message to %s: %s", firebaseControlTopic, err.Error())
|
||||||
|
}
|
||||||
|
case <-time.After(s.config.FirebasePollInterval):
|
||||||
|
if err := s.firebase(newKeepaliveMessage(firebasePollTopic)); err != nil {
|
||||||
|
log.Printf("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error())
|
||||||
}
|
}
|
||||||
case <-s.closeChan:
|
case <-s.closeChan:
|
||||||
return
|
return
|
||||||
@@ -1118,6 +1166,15 @@ func (s *Server) limitRequests(next handleFunc) handleFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
|
if !s.config.EnableWeb {
|
||||||
|
return errHTTPNotFound
|
||||||
|
}
|
||||||
|
return next(w, r, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
|
// transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
|
||||||
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
|
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
|
||||||
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# ntfy server config file
|
# ntfy server config file
|
||||||
|
#
|
||||||
|
# Please refer to the documentation at https://ntfy.sh/docs/config/ for details.
|
||||||
|
# All options also support underscores (_) instead of dashes (-) to comply with the YAML spec.
|
||||||
|
|
||||||
# Public facing base URL of the service (e.g. https://ntfy.sh or https://ntfy.example.com)
|
# Public facing base URL of the service (e.g. https://ntfy.sh or https://ntfy.example.com)
|
||||||
# This setting is currently only used by the attachments and e-mail sending feature (outgoing mail only).
|
# This setting is currently only used by the attachments and e-mail sending feature (outgoing mail only).
|
||||||
@@ -127,10 +130,23 @@
|
|||||||
# manager-interval: "1m"
|
# manager-interval: "1m"
|
||||||
|
|
||||||
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
|
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
|
||||||
# web app. If you self-host, you don't want to change this. Can be "app" (default) or "home".
|
# web app. If you self-host, you don't want to change this.
|
||||||
|
# Can be "app" (default), "home" or "disable" to disable the web app entirely.
|
||||||
#
|
#
|
||||||
# web-root: app
|
# web-root: app
|
||||||
|
|
||||||
|
# Server URL of a Firebase/APNS-connected ntfy server (likely "https://ntfy.sh").
|
||||||
|
#
|
||||||
|
# iOS users:
|
||||||
|
# If you use the iOS ntfy app, you MUST configure this to receive timely notifications. You'll like want this:
|
||||||
|
# upstream-base-url: "https://ntfy.sh"
|
||||||
|
#
|
||||||
|
# If set, all incoming messages will publish a "poll_request" message to the configured upstream server, containing
|
||||||
|
# the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
|
||||||
|
# This is to prevent the upstream server and Firebase/APNS from being able to read the message.
|
||||||
|
#
|
||||||
|
# upstream-base-url:
|
||||||
|
|
||||||
# Rate limiting: Total number of topics before the server rejects new topics.
|
# Rate limiting: Total number of topics before the server rejects new topics.
|
||||||
#
|
#
|
||||||
# global-topic-limit: 15000
|
# global-topic-limit: 15000
|
||||||
|
|||||||
@@ -3,37 +3,20 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
firebase "firebase.google.com/go"
|
firebase "firebase.google.com/go"
|
||||||
"firebase.google.com/go/messaging"
|
"firebase.google.com/go/messaging"
|
||||||
"fmt"
|
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
"heckel.io/ntfy/auth"
|
"heckel.io/ntfy/auth"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
fcmMessageLimit = 4000
|
fcmMessageLimit = 4000
|
||||||
|
fcmApnsBodyMessageLimit = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// maybeTruncateFCMMessage performs best-effort truncation of FCM messages.
|
|
||||||
// The docs say the limit is 4000 characters, but during testing it wasn't quite clear
|
|
||||||
// what fields matter; so we're just capping the serialized JSON to 4000 bytes.
|
|
||||||
func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
|
|
||||||
s, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
if len(s) > fcmMessageLimit {
|
|
||||||
over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ...
|
|
||||||
message, ok := m.Data["message"]
|
|
||||||
if ok && len(message) > over {
|
|
||||||
m.Data["truncated"] = "1"
|
|
||||||
m.Data["message"] = message[:len(message)-over]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) {
|
func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) {
|
||||||
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
|
fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,8 +36,29 @@ func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subsc
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toFirebaseMessage converts a message to a Firebase message.
|
||||||
|
//
|
||||||
|
// Normal messages ("message"):
|
||||||
|
// - For Android, we can receive data messages from Firebase and process them as code, so we just send all fields
|
||||||
|
// in the "data" attribute. In the Android app, we then turn those into a notification and display it.
|
||||||
|
// - On iOS, we are not allowed to receive data-only messages, so we build messages with an "alert" (with title and
|
||||||
|
// message), and still send the rest of the data along in the "aps" attribute. We can then locally modify the
|
||||||
|
// message in the Notification Service Extension.
|
||||||
|
//
|
||||||
|
// Keepalive messages ("keepalive"):
|
||||||
|
// - On Android, we subscribe to the "~control" topic, which is used to restart the foreground service (if it died,
|
||||||
|
// e.g. after an app update). We send these keepalive messages regularly (see Config.FirebaseKeepaliveInterval).
|
||||||
|
// - On iOS, we subscribe to the "~poll" topic, which is used to poll all topics regularly. This is because iOS
|
||||||
|
// does not allow any background or scheduled activity at all.
|
||||||
|
//
|
||||||
|
// Poll request messages ("poll_request"):
|
||||||
|
// - Normal messages are turned into poll request messages if anonymous users are not allowed to read the message.
|
||||||
|
// On Android, this will trigger the app to poll the topic and thereby displaying new messages.
|
||||||
|
// - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded
|
||||||
|
// to Firebase here. This is mainly for iOS to support self-hosted servers.
|
||||||
func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, error) {
|
func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, error) {
|
||||||
var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
|
var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
|
||||||
|
var apnsConfig *messaging.APNSConfig
|
||||||
switch m.Event {
|
switch m.Event {
|
||||||
case keepaliveEvent, openEvent:
|
case keepaliveEvent, openEvent:
|
||||||
data = map[string]string{
|
data = map[string]string{
|
||||||
@@ -63,6 +67,17 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
|||||||
"event": m.Event,
|
"event": m.Event,
|
||||||
"topic": m.Topic,
|
"topic": m.Topic,
|
||||||
}
|
}
|
||||||
|
apnsConfig = createAPNSBackgroundConfig(data)
|
||||||
|
case pollRequestEvent:
|
||||||
|
data = map[string]string{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": m.Event,
|
||||||
|
"topic": m.Topic,
|
||||||
|
"message": m.Message,
|
||||||
|
"poll_id": m.PollID,
|
||||||
|
}
|
||||||
|
apnsConfig = createAPNSAlertConfig(m, data)
|
||||||
case messageEvent:
|
case messageEvent:
|
||||||
allowForward := true
|
allowForward := true
|
||||||
if auther != nil {
|
if auther != nil {
|
||||||
@@ -95,6 +110,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
|||||||
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
|
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
|
||||||
data["attachment_url"] = m.Attachment.URL
|
data["attachment_url"] = m.Attachment.URL
|
||||||
}
|
}
|
||||||
|
apnsConfig = createAPNSAlertConfig(m, data)
|
||||||
} else {
|
} else {
|
||||||
// If anonymous read for a topic is not allowed, we cannot send the message along
|
// If anonymous read for a topic is not allowed, we cannot send the message along
|
||||||
// via Firebase. Instead, we send a "poll_request" message, asking the client to poll.
|
// via Firebase. Instead, we send a "poll_request" message, asking the client to poll.
|
||||||
@@ -104,6 +120,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
|||||||
"event": pollRequestEvent,
|
"event": pollRequestEvent,
|
||||||
"topic": m.Topic,
|
"topic": m.Topic,
|
||||||
}
|
}
|
||||||
|
// TODO Handle APNS?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var androidConfig *messaging.AndroidConfig
|
var androidConfig *messaging.AndroidConfig
|
||||||
@@ -116,5 +133,85 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
|
|||||||
Topic: m.Topic,
|
Topic: m.Topic,
|
||||||
Data: data,
|
Data: data,
|
||||||
Android: androidConfig,
|
Android: androidConfig,
|
||||||
|
APNS: apnsConfig,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maybeTruncateFCMMessage performs best-effort truncation of FCM messages.
|
||||||
|
// The docs say the limit is 4000 characters, but during testing it wasn't quite clear
|
||||||
|
// what fields matter; so we're just capping the serialized JSON to 4000 bytes.
|
||||||
|
func maybeTruncateFCMMessage(m *messaging.Message) *messaging.Message {
|
||||||
|
s, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
if len(s) > fcmMessageLimit {
|
||||||
|
over := len(s) - fcmMessageLimit + 16 // = len("truncated":"1",), sigh ...
|
||||||
|
message, ok := m.Data["message"]
|
||||||
|
if ok && len(message) > over {
|
||||||
|
m.Data["truncated"] = "1"
|
||||||
|
m.Data["message"] = message[:len(message)-over]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAPNSAlertConfig creates an APNS config for iOS notifications that show up as an alert (only relevant for iOS).
|
||||||
|
// We must set the Alert struct ("alert"), and we need to set MutableContent ("mutable-content"), so the Notification Service
|
||||||
|
// Extension in iOS can modify the message.
|
||||||
|
func createAPNSAlertConfig(m *message, data map[string]string) *messaging.APNSConfig {
|
||||||
|
apnsData := make(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
apnsData[k] = v
|
||||||
|
}
|
||||||
|
return &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
CustomData: apnsData,
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
MutableContent: true,
|
||||||
|
Alert: &messaging.ApsAlert{
|
||||||
|
Title: m.Title,
|
||||||
|
Body: maybeTruncateAPNSBodyMessage(m.Message),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createAPNSBackgroundConfig creates an APNS config for a silent background message (only relevant for iOS). Apple only
|
||||||
|
// allows us to send 2-3 of these notifications per hour, and delivery not guaranteed. We use this only for the ~poll
|
||||||
|
// topic, which triggers the iOS app to poll all topics for changes.
|
||||||
|
//
|
||||||
|
// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
|
||||||
|
func createAPNSBackgroundConfig(data map[string]string) *messaging.APNSConfig {
|
||||||
|
apnsData := make(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
apnsData[k] = v
|
||||||
|
}
|
||||||
|
return &messaging.APNSConfig{
|
||||||
|
Headers: map[string]string{
|
||||||
|
"apns-push-type": "background",
|
||||||
|
"apns-priority": "5",
|
||||||
|
},
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
ContentAvailable: true,
|
||||||
|
},
|
||||||
|
CustomData: apnsData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeTruncateAPNSBodyMessage truncates the body for APNS.
|
||||||
|
//
|
||||||
|
// The "body" of the push notification can contain the entire message, which would count doubly for the overall length
|
||||||
|
// of the APNS payload. I set a limit of 100 characters before truncating the notification "body" with ellipsis.
|
||||||
|
// The message would not be changed (unless truncated for being too long). Note: if the payload is too large (>4KB),
|
||||||
|
// APNS will simply reject / discard the notification, meaning it will never arrive on the iOS device.
|
||||||
|
func maybeTruncateAPNSBodyMessage(s string) string {
|
||||||
|
if len(s) >= fcmApnsBodyMessageLimit {
|
||||||
|
over := len(s) - fcmApnsBodyMessageLimit + 3 // len("...")
|
||||||
|
return s[:len(s)-over] + "..."
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,23 @@ func TestToFirebaseMessage_Keepalive(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, "mytopic", fbm.Topic)
|
require.Equal(t, "mytopic", fbm.Topic)
|
||||||
require.Nil(t, fbm.Android)
|
require.Nil(t, fbm.Android)
|
||||||
|
require.Equal(t, &messaging.APNSConfig{
|
||||||
|
Headers: map[string]string{
|
||||||
|
"apns-push-type": "background",
|
||||||
|
"apns-priority": "5",
|
||||||
|
},
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
ContentAvailable: true,
|
||||||
|
},
|
||||||
|
CustomData: map[string]interface{}{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": m.Event,
|
||||||
|
"topic": m.Topic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, fbm.APNS)
|
||||||
require.Equal(t, map[string]string{
|
require.Equal(t, map[string]string{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"time": fmt.Sprintf("%d", m.Time),
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
@@ -46,6 +63,23 @@ func TestToFirebaseMessage_Open(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, "mytopic", fbm.Topic)
|
require.Equal(t, "mytopic", fbm.Topic)
|
||||||
require.Nil(t, fbm.Android)
|
require.Nil(t, fbm.Android)
|
||||||
|
require.Equal(t, &messaging.APNSConfig{
|
||||||
|
Headers: map[string]string{
|
||||||
|
"apns-push-type": "background",
|
||||||
|
"apns-priority": "5",
|
||||||
|
},
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
ContentAvailable: true,
|
||||||
|
},
|
||||||
|
CustomData: map[string]interface{}{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": m.Event,
|
||||||
|
"topic": m.Topic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, fbm.APNS)
|
||||||
require.Equal(t, map[string]string{
|
require.Equal(t, map[string]string{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"time": fmt.Sprintf("%d", m.Time),
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
@@ -60,6 +94,25 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
|||||||
m.Tags = []string{"tag 1", "tag2"}
|
m.Tags = []string{"tag 1", "tag2"}
|
||||||
m.Click = "https://google.com"
|
m.Click = "https://google.com"
|
||||||
m.Title = "some title"
|
m.Title = "some title"
|
||||||
|
m.Actions = []*action{
|
||||||
|
{
|
||||||
|
ID: "123",
|
||||||
|
Action: "view",
|
||||||
|
Label: "Open page",
|
||||||
|
Clear: true,
|
||||||
|
URL: "https://ntfy.sh",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "456",
|
||||||
|
Action: "http",
|
||||||
|
Label: "Close door",
|
||||||
|
URL: "https://door.com/close",
|
||||||
|
Method: "PUT",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"really": "yes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
m.Attachment = &attachment{
|
m.Attachment = &attachment{
|
||||||
Name: "some file.jpg",
|
Name: "some file.jpg",
|
||||||
Type: "image/jpeg",
|
Type: "image/jpeg",
|
||||||
@@ -74,6 +127,35 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
|||||||
require.Equal(t, &messaging.AndroidConfig{
|
require.Equal(t, &messaging.AndroidConfig{
|
||||||
Priority: "high",
|
Priority: "high",
|
||||||
}, fbm.Android)
|
}, fbm.Android)
|
||||||
|
require.Equal(t, &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
MutableContent: true,
|
||||||
|
Alert: &messaging.ApsAlert{
|
||||||
|
Title: "some title",
|
||||||
|
Body: "this is a message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CustomData: map[string]interface{}{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": "message",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"priority": "4",
|
||||||
|
"tags": strings.Join(m.Tags, ","),
|
||||||
|
"click": "https://google.com",
|
||||||
|
"title": "some title",
|
||||||
|
"message": "this is a message",
|
||||||
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||||
|
"encoding": "",
|
||||||
|
"attachment_name": "some file.jpg",
|
||||||
|
"attachment_type": "image/jpeg",
|
||||||
|
"attachment_size": "12345",
|
||||||
|
"attachment_expires": "98765543",
|
||||||
|
"attachment_url": "https://example.com/file.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, fbm.APNS)
|
||||||
require.Equal(t, map[string]string{
|
require.Equal(t, map[string]string{
|
||||||
"id": m.ID,
|
"id": m.ID,
|
||||||
"time": fmt.Sprintf("%d", m.Time),
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
@@ -84,6 +166,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
|||||||
"click": "https://google.com",
|
"click": "https://google.com",
|
||||||
"title": "some title",
|
"title": "some title",
|
||||||
"message": "this is a message",
|
"message": "this is a message",
|
||||||
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
||||||
"encoding": "",
|
"encoding": "",
|
||||||
"attachment_name": "some file.jpg",
|
"attachment_name": "some file.jpg",
|
||||||
"attachment_type": "image/jpeg",
|
"attachment_type": "image/jpeg",
|
||||||
@@ -112,6 +195,41 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
|
|||||||
}, fbm.Data)
|
}, fbm.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToFirebaseMessage_PollRequest(t *testing.T) {
|
||||||
|
m := newPollRequestMessage("mytopic", "fOv6k1QbCzo6")
|
||||||
|
fbm, err := toFirebaseMessage(m, nil)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "mytopic", fbm.Topic)
|
||||||
|
require.Nil(t, fbm.Android)
|
||||||
|
require.Equal(t, &messaging.APNSConfig{
|
||||||
|
Payload: &messaging.APNSPayload{
|
||||||
|
Aps: &messaging.Aps{
|
||||||
|
MutableContent: true,
|
||||||
|
Alert: &messaging.ApsAlert{
|
||||||
|
Title: "",
|
||||||
|
Body: "New message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CustomData: map[string]interface{}{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": "poll_request",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "New message",
|
||||||
|
"poll_id": "fOv6k1QbCzo6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, fbm.APNS)
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"id": m.ID,
|
||||||
|
"time": fmt.Sprintf("%d", m.Time),
|
||||||
|
"event": "poll_request",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "New message",
|
||||||
|
"poll_id": "fOv6k1QbCzo6",
|
||||||
|
}, fbm.Data)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMaybeTruncateFCMMessage(t *testing.T) {
|
func TestMaybeTruncateFCMMessage(t *testing.T) {
|
||||||
origMessage := strings.Repeat("this is a long string", 300)
|
origMessage := strings.Repeat("this is a long string", 300)
|
||||||
origFCMMessage := &messaging.Message{
|
origFCMMessage := &messaging.Message{
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"heckel.io/ntfy/auth"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -18,6 +15,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/auth"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_PublishAndPoll(t *testing.T) {
|
func TestServer_PublishAndPoll(t *testing.T) {
|
||||||
@@ -162,6 +163,40 @@ func TestServer_StaticSites(t *testing.T) {
|
|||||||
require.Contains(t, rr.Body.String(), "</html>")
|
require.Contains(t, rr.Body.String(), "</html>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_WebEnabled(t *testing.T) {
|
||||||
|
conf := newTestConfig(t)
|
||||||
|
conf.EnableWeb = false
|
||||||
|
s := newTestServer(t, conf)
|
||||||
|
|
||||||
|
rr := request(t, s, "GET", "/", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/example.html", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/config.js", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
conf2 := newTestConfig(t)
|
||||||
|
conf2.EnableWeb = true
|
||||||
|
s2 := newTestServer(t, conf2)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/example.html", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/static/css/home.css", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishLargeMessage(t *testing.T) {
|
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
c.AttachmentCacheDir = "" // Disable attachments
|
c.AttachmentCacheDir = "" // Disable attachments
|
||||||
@@ -1303,7 +1338,7 @@ func firebaseServiceAccountFile(t *testing.T) string {
|
|||||||
return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
|
return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
|
||||||
} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
|
} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
|
||||||
filename := filepath.Join(t.TempDir(), "firebase.json")
|
filename := filepath.Join(t.TempDir(), "firebase.json")
|
||||||
require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0600))
|
require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0o600))
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
|
|||||||
@@ -159,37 +159,47 @@ func (s *smtpSession) withFailCount(fn func() error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readMailBody(msg *mail.Message) (string, error) {
|
func readMailBody(msg *mail.Message) (string, error) {
|
||||||
|
if msg.Header.Get("Content-Type") == "" {
|
||||||
|
return readPlainTextMailBody(msg)
|
||||||
|
}
|
||||||
contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
|
contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if contentType == "text/plain" {
|
if contentType == "text/plain" {
|
||||||
body, err := io.ReadAll(msg.Body)
|
return readPlainTextMailBody(msg)
|
||||||
|
} else if strings.HasPrefix(contentType, "multipart/") {
|
||||||
|
return readMultipartMailBody(msg, params)
|
||||||
|
}
|
||||||
|
return "", errUnsupportedContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPlainTextMailBody(msg *mail.Message) (string, error) {
|
||||||
|
body, err := io.ReadAll(msg.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(body), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMultipartMailBody(msg *mail.Message, params map[string]string) (string, error) {
|
||||||
|
mr := multipart.NewReader(msg.Body, params["boundary"])
|
||||||
|
for {
|
||||||
|
part, err := mr.NextPart()
|
||||||
|
if err != nil { // may be io.EOF
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if partContentType != "text/plain" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return string(body), nil
|
return string(body), nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(contentType, "multipart/") {
|
|
||||||
mr := multipart.NewReader(msg.Body, params["boundary"])
|
|
||||||
for {
|
|
||||||
part, err := mr.NextPart()
|
|
||||||
if err != nil { // may be io.EOF
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
partContentType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if partContentType != "text/plain" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(part)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(body), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errUnsupportedContentType
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,24 @@ what's up
|
|||||||
require.Nil(t, session.Data(strings.NewReader(email)))
|
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) {
|
||||||
|
email := `Subject: Very short mail
|
||||||
|
|
||||||
|
what's up
|
||||||
|
`
|
||||||
|
conf, backend := newTestBackend(t, func(m *message) error {
|
||||||
|
require.Equal(t, "mytopic", m.Topic)
|
||||||
|
require.Equal(t, "Very short mail", m.Title)
|
||||||
|
require.Equal(t, "what's up", m.Message)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
conf.SMTPServerAddrPrefix = ""
|
||||||
|
session, _ := backend.AnonymousLogin(nil)
|
||||||
|
require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{}))
|
||||||
|
require.Nil(t, session.Rcpt("mytopic@ntfy.sh"))
|
||||||
|
require.Nil(t, session.Data(strings.NewReader(email)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
|
func TestSmtpBackend_Plaintext_EncodedSubject(t *testing.T) {
|
||||||
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
email := `Date: Tue, 28 Dec 2021 00:30:10 +0100
|
||||||
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
|
Subject: =?UTF-8?B?VGhyZWUgc2FudGFzIPCfjoXwn46F8J+OhQ==?=
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ type message struct {
|
|||||||
Time int64 `json:"time"` // Unix time in seconds
|
Time int64 `json:"time"` // Unix time in seconds
|
||||||
Event string `json:"event"` // One of the above
|
Event string `json:"event"` // One of the above
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Click string `json:"click,omitempty"`
|
Click string `json:"click,omitempty"`
|
||||||
Actions []*action `json:"actions,omitempty"`
|
Actions []*action `json:"actions,omitempty"`
|
||||||
Attachment *attachment `json:"attachment,omitempty"`
|
Attachment *attachment `json:"attachment,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
PollID string `json:"poll_id,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,14 +85,11 @@ type messageEncoder func(msg *message) (string, error)
|
|||||||
// newMessage creates a new message with the current timestamp
|
// newMessage creates a new message with the current timestamp
|
||||||
func newMessage(event, topic, msg string) *message {
|
func newMessage(event, topic, msg string) *message {
|
||||||
return &message{
|
return &message{
|
||||||
ID: util.RandomString(messageIDLength),
|
ID: util.RandomString(messageIDLength),
|
||||||
Time: time.Now().Unix(),
|
Time: time.Now().Unix(),
|
||||||
Event: event,
|
Event: event,
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
Priority: 0,
|
Message: msg,
|
||||||
Tags: nil,
|
|
||||||
Title: "",
|
|
||||||
Message: msg,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +108,13 @@ func newDefaultMessage(topic, msg string) *message {
|
|||||||
return newMessage(messageEvent, topic, msg)
|
return newMessage(messageEvent, topic, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newPollRequestMessage is a convenience method to create a poll request message
|
||||||
|
func newPollRequestMessage(topic, pollID string) *message {
|
||||||
|
m := newMessage(pollRequestEvent, topic, newMessageBody)
|
||||||
|
m.PollID = pollID
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
func validMessageID(s string) bool {
|
func validMessageID(s string) bool {
|
||||||
return util.ValidRandomString(s, messageIDLength)
|
return util.ValidRandomString(s, messageIDLength)
|
||||||
}
|
}
|
||||||
@@ -153,6 +158,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type queryFilter struct {
|
type queryFilter struct {
|
||||||
|
ID string
|
||||||
Message string
|
Message string
|
||||||
Title string
|
Title string
|
||||||
Tags []string
|
Tags []string
|
||||||
@@ -160,6 +166,7 @@ type queryFilter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
||||||
|
idFilter := readParam(r, "x-id", "id")
|
||||||
messageFilter := readParam(r, "x-message", "message", "m")
|
messageFilter := readParam(r, "x-message", "message", "m")
|
||||||
titleFilter := readParam(r, "x-title", "title", "t")
|
titleFilter := readParam(r, "x-title", "title", "t")
|
||||||
tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
|
tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
|
||||||
@@ -172,6 +179,7 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
|||||||
priorityFilter = append(priorityFilter, priority)
|
priorityFilter = append(priorityFilter, priority)
|
||||||
}
|
}
|
||||||
return &queryFilter{
|
return &queryFilter{
|
||||||
|
ID: idFilter,
|
||||||
Message: messageFilter,
|
Message: messageFilter,
|
||||||
Title: titleFilter,
|
Title: titleFilter,
|
||||||
Tags: tagsFilter,
|
Tags: tagsFilter,
|
||||||
@@ -182,11 +190,11 @@ func parseQueryFilters(r *http.Request) (*queryFilter, error) {
|
|||||||
func (q *queryFilter) Pass(msg *message) bool {
|
func (q *queryFilter) Pass(msg *message) bool {
|
||||||
if msg.Event != messageEvent {
|
if msg.Event != messageEvent {
|
||||||
return true // filters only apply to messages
|
return true // filters only apply to messages
|
||||||
}
|
} else if q.ID != "" && msg.ID != q.ID {
|
||||||
if q.Message != "" && msg.Message != q.Message {
|
|
||||||
return false
|
return false
|
||||||
}
|
} else if q.Message != "" && msg.Message != q.Message {
|
||||||
if q.Title != "" && msg.Title != q.Title {
|
return false
|
||||||
|
} else if q.Title != "" && msg.Title != q.Title {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
messagePriority := msg.Priority
|
messagePriority := msg.Priority
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func TestSniffWriter_WriteUnknownMimeType(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
sw := NewContentTypeWriter(rr, "")
|
sw := NewContentTypeWriter(rr, "")
|
||||||
randomBytes := make([]byte, 199)
|
randomBytes := make([]byte, 199)
|
||||||
rand.Read(randomBytes)
|
rand.Read(randomBytes[5:]) // Start at an offset; the test kept failing randomly because it hit random magic strings
|
||||||
sw.Write(randomBytes)
|
sw.Write(randomBytes)
|
||||||
require.Equal(t, "application/octet-stream", rr.Header().Get("Content-Type"))
|
require.Equal(t, "application/octet-stream", rr.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,11 +183,6 @@ func PriorityString(priority int) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpandHome replaces "~" with the user's home directory
|
|
||||||
func ExpandHome(path string) string {
|
|
||||||
return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortTopicURL shortens the topic URL to be human-friendly, removing the http:// or https://
|
// ShortTopicURL shortens the topic URL to be human-friendly, removing the http:// or https://
|
||||||
func ShortTopicURL(s string) string {
|
func ShortTopicURL(s string) string {
|
||||||
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
|
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package util
|
|||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -75,14 +74,6 @@ func TestSplitNoEmpty(t *testing.T) {
|
|||||||
require.Equal(t, []string{"tag1", "tag2"}, SplitNoEmpty("tag1,tag2,", ","))
|
require.Equal(t, []string{"tag1", "tag2"}, SplitNoEmpty("tag1,tag2,", ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpandHome_WithTilde(t *testing.T) {
|
|
||||||
require.Equal(t, os.Getenv("HOME")+"/this/is/a/path", ExpandHome("~/this/is/a/path"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpandHome_NoTilde(t *testing.T) {
|
|
||||||
require.Equal(t, "/this/is/an/absolute/path", ExpandHome("/this/is/an/absolute/path"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePriority(t *testing.T) {
|
func TestParsePriority(t *testing.T) {
|
||||||
priorities := []string{"", "1", "2", "3", "4", "5", "min", "LOW", " default ", "HIgh", "max", "urgent"}
|
priorities := []string{"", "1", "2", "3", "4", "5", "min", "LOW", " default ", "HIgh", "max", "urgent"}
|
||||||
expected := []int{0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 5}
|
expected := []int{0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 5}
|
||||||
|
|||||||
4279
web/package-lock.json
generated
4279
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
"publish_dialog_progress_uploading": "Изпращане…",
|
"publish_dialog_progress_uploading": "Изпращане…",
|
||||||
"publish_dialog_progress_uploading_detail": "Изпращане {{loaded}}/{{total}} ({{percent}}%)…",
|
"publish_dialog_progress_uploading_detail": "Изпращане {{loaded}}/{{total}} ({{percent}}%)…",
|
||||||
"publish_dialog_message_published": "Известието е публикувано",
|
"publish_dialog_message_published": "Известието е публикувано",
|
||||||
"publish_dialog_attachment_limits_file_and_quota_reached": "надвишава ограничението и квотата от {{fileSizeLimit}}, оставащи {{remainingBytes}}",
|
"publish_dialog_attachment_limits_file_and_quota_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл и квотата, остават {{remainingBytes}}",
|
||||||
"publish_dialog_message_label": "Съобщение",
|
"publish_dialog_message_label": "Съобщение",
|
||||||
"publish_dialog_message_placeholder": "Въведете съобщение",
|
"publish_dialog_message_placeholder": "Въведете съобщение",
|
||||||
"publish_dialog_other_features": "Други възможности:",
|
"publish_dialog_other_features": "Други възможности:",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"message_bar_type_message": "Въведете съобщение",
|
"message_bar_type_message": "Въведете съобщение",
|
||||||
"message_bar_error_publishing": "Грешка при изпращане на известието",
|
"message_bar_error_publishing": "Грешка при изпращане на известието",
|
||||||
"notifications_copied_to_clipboard": "Копирано в междинната памет",
|
"notifications_copied_to_clipboard": "Копирано в междинната памет",
|
||||||
"notifications_attachment_link_expired": "препратката за изтегляне е невалидна",
|
"notifications_attachment_link_expired": "препратката за изтегляне е с изтекла давност",
|
||||||
"nav_button_settings": "Настройки",
|
"nav_button_settings": "Настройки",
|
||||||
"nav_button_documentation": "Ръководство",
|
"nav_button_documentation": "Ръководство",
|
||||||
"nav_button_subscribe": "Абониране за тема",
|
"nav_button_subscribe": "Абониране за тема",
|
||||||
@@ -59,27 +59,27 @@
|
|||||||
"notifications_actions_open_url_title": "Към {{url}}",
|
"notifications_actions_open_url_title": "Към {{url}}",
|
||||||
"notifications_click_copy_url_button": "Копиране на препратка",
|
"notifications_click_copy_url_button": "Копиране на препратка",
|
||||||
"notifications_click_open_button": "Отваряне",
|
"notifications_click_open_button": "Отваряне",
|
||||||
"notifications_click_copy_url_title": "Копира препратката в междинната памет",
|
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
|
||||||
"notifications_none_for_topic_title": "Липсват известия в темата",
|
"notifications_none_for_topic_title": "Липсват известия в темата",
|
||||||
"notifications_none_for_any_title": "Липсват известия",
|
"notifications_none_for_any_title": "Липсват известия",
|
||||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема, просто изпратете PUT или POST към адреса ѝ.",
|
"notifications_none_for_topic_description": "За да изпратите известия в тази тема, просто направете PUT или POST към адреса ѝ.",
|
||||||
"notifications_none_for_any_description": "За да изпратите известия в тема, просто изпратете PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
"notifications_none_for_any_description": "За да изпратите известия в тема, просто направете PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
||||||
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като изпратите съобщения чрез метода PUT или POST ще ги получавате тук.",
|
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като изпратите съобщения чрез метода PUT или POST ще ги получите тук.",
|
||||||
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
||||||
"publish_dialog_priority_min": "Мин. приоритет",
|
"publish_dialog_priority_min": "Мин. приоритет",
|
||||||
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}}",
|
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}} за размер на файл",
|
||||||
"publish_dialog_base_url_label": "Адрес на услугата",
|
"publish_dialog_base_url_label": "Адрес на услугата",
|
||||||
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
|
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
|
||||||
"publish_dialog_topic_placeholder": "Име на темата, напр. phils_alerts",
|
"publish_dialog_topic_placeholder": "Име на темата, напр. phils_alerts",
|
||||||
"publish_dialog_priority_low": "Нисък приоритет",
|
"publish_dialog_priority_low": "Нисък приоритет",
|
||||||
"publish_dialog_attachment_limits_quota_reached": "надвишава ограничението, оставащи {{remainingBytes}}",
|
"publish_dialog_attachment_limits_quota_reached": "надвишава квотата, остават {{remainingBytes}}",
|
||||||
"publish_dialog_priority_high": "Висок приоритет",
|
"publish_dialog_priority_high": "Висок приоритет",
|
||||||
"publish_dialog_priority_default": "Подразбиран приоритет",
|
"publish_dialog_priority_default": "Подразбиран приоритет",
|
||||||
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
|
||||||
"publish_dialog_tags_label": "Етикети",
|
"publish_dialog_tags_label": "Етикети",
|
||||||
"publish_dialog_email_label": "Адрес на електронна поща",
|
"publish_dialog_email_label": "Адрес на електронна поща",
|
||||||
"publish_dialog_priority_max": "Макс. приоритет",
|
"publish_dialog_priority_max": "Макс. приоритет",
|
||||||
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. внимание, диск",
|
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. warning, srv1-backup",
|
||||||
"publish_dialog_click_label": "Адрес",
|
"publish_dialog_click_label": "Адрес",
|
||||||
"publish_dialog_topic_label": "Име на темата",
|
"publish_dialog_topic_label": "Име на темата",
|
||||||
"publish_dialog_title_label": "Заглавие",
|
"publish_dialog_title_label": "Заглавие",
|
||||||
@@ -130,14 +130,14 @@
|
|||||||
"prefs_users_dialog_username_label": "Потребител, напр. phil",
|
"prefs_users_dialog_username_label": "Потребител, напр. phil",
|
||||||
"prefs_users_dialog_button_add": "Добавяне",
|
"prefs_users_dialog_button_add": "Добавяне",
|
||||||
"error_boundary_title": "О, не, ntfy се срина",
|
"error_boundary_title": "О, не, ntfy се срина",
|
||||||
"error_boundary_description": "Това очевидно не трябва да се случва. Много съжаляваме!<br/>Ако имате минута, <githubLink>докладвайте в GitHub</githubLink>, или ни уведомете в <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
|
"error_boundary_description": "Това очевидно не трябва да се случва. Много съжаляваме!<br/>Ако имате минута, <githubLink>докладвайте в GitHub</githubLink> или ни уведомете в <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
|
||||||
"error_boundary_stack_trace": "Следа от стека",
|
"error_boundary_stack_trace": "Следа от стека",
|
||||||
"error_boundary_gathering_info": "Събиране на допълнителна информация…",
|
"error_boundary_gathering_info": "Събиране на допълнителна информация…",
|
||||||
"notifications_loading": "Зареждане на известия…",
|
"notifications_loading": "Зареждане на известия…",
|
||||||
"error_boundary_button_copy_stack_trace": "Копиране на следата от стека",
|
"error_boundary_button_copy_stack_trace": "Копиране на следата от стека",
|
||||||
"prefs_users_description": "Добавяйте и премахвайте потребители за защитените теми. Имайте предвид, че потребителското име и паролата се съхраняват в местната памет на мрежовия четец.",
|
"prefs_users_description": "Добавяйте и премахвайте потребители за защитените теми. Имайте предвид, че потребителското име и паролата се съхраняват в местната памет на мрежовия четец.",
|
||||||
"prefs_notifications_sound_description_none": "Известията не са съпроводени със звук",
|
"prefs_notifications_sound_description_none": "Известията не са съпроводени със звук",
|
||||||
"prefs_notifications_sound_description_some": "Известията са съпроводени със звука „{{sound}}“",
|
"prefs_notifications_sound_description_some": "При пристигане известията са съпроводени от звука „{{sound}}“",
|
||||||
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
|
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
|
||||||
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
|
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
|
||||||
"priority_min": "минимален",
|
"priority_min": "минимален",
|
||||||
@@ -149,8 +149,43 @@
|
|||||||
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
|
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
|
||||||
"prefs_notifications_min_priority_description_max": "Показват се известията с приоритет 5 (най-висок)",
|
"prefs_notifications_min_priority_description_max": "Показват се известията с приоритет 5 (най-висок)",
|
||||||
"prefs_notifications_delete_after_one_month_description": "Известията се премахват автоматично след един месец",
|
"prefs_notifications_delete_after_one_month_description": "Известията се премахват автоматично след един месец",
|
||||||
"prefs_notifications_min_priority_description_any": "Показват се всички известия, независимо от приоритета им",
|
"prefs_notifications_min_priority_description_any": "Показват се всички известия, независимо от приоритета",
|
||||||
"prefs_notifications_min_priority_description_x_or_higher": "Показват се известията с приоритет {{number}} ({{name}}) или по-висок",
|
"prefs_notifications_min_priority_description_x_or_higher": "Показват се известията с приоритет {{number}} ({{name}}) или по-висок",
|
||||||
"notifications_actions_http_request_title": "Изпращане на HTTP {{method}} до {{url}}",
|
"notifications_actions_http_request_title": "Изпращане на HTTP {{method}} до {{url}}",
|
||||||
"notifications_actions_not_supported": "Действието не се поддържа от приложението за уеб"
|
"notifications_actions_not_supported": "Действието не се поддържа от приложението за интернет",
|
||||||
|
"action_bar_show_menu": "Показване на менюто",
|
||||||
|
"action_bar_logo_alt": "Логотип на ntfy",
|
||||||
|
"action_bar_toggle_mute": "Заглушаване или пускне на известията",
|
||||||
|
"action_bar_toggle_action_menu": "Отваряне или затваряне на менюто с действията",
|
||||||
|
"nav_button_muted": "Известията са заглушени",
|
||||||
|
"notifications_list": "Списък с известия",
|
||||||
|
"notifications_list_item": "Известие",
|
||||||
|
"notifications_delete": "Изтриване",
|
||||||
|
"notifications_mark_read": "Отбелязване като прочетено",
|
||||||
|
"nav_button_connecting": "свързване",
|
||||||
|
"message_bar_show_dialog": "Показване на диалога за публикуване",
|
||||||
|
"message_bar_publish": "Публикуване на съобщение",
|
||||||
|
"notifications_priority_x": "Приоритет {{priority}}",
|
||||||
|
"notifications_new_indicator": "Ново известие",
|
||||||
|
"notifications_attachment_image": "Прикачено изображение",
|
||||||
|
"notifications_attachment_file_image": "файл на изображение",
|
||||||
|
"notifications_attachment_file_video": "файл на видео",
|
||||||
|
"notifications_attachment_file_audio": "файл на аудио",
|
||||||
|
"notifications_attachment_file_app": "Инсталационен файл на приложение за Android",
|
||||||
|
"notifications_attachment_file_document": "друг документ",
|
||||||
|
"publish_dialog_emoji_picker_show": "Избор на емоция",
|
||||||
|
"publish_dialog_topic_reset": "Нулиране на тема",
|
||||||
|
"publish_dialog_click_reset": "Премахване на адрес",
|
||||||
|
"publish_dialog_email_reset": "Премахване на препращането към ел. поща",
|
||||||
|
"publish_dialog_delay_reset": "Премахва забавянето на изпращането",
|
||||||
|
"publish_dialog_attached_file_remove": "Премахване на прикачения файл",
|
||||||
|
"emoji_picker_search_clear": "Изчистване на търсенето",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "Адрес на услугата",
|
||||||
|
"prefs_notifications_sound_play": "Възпроизвеждане на избрания звук",
|
||||||
|
"publish_dialog_attach_reset": "Премахване на адреса на файла за прикачане",
|
||||||
|
"prefs_users_delete_button": "Премахване на потребител",
|
||||||
|
"prefs_users_table": "Таблица с потребители",
|
||||||
|
"prefs_users_edit_button": "Промяна на потребител",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Поверително разглеждане не се поддържа",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "За да работи интернет-приложението ntfy се нуждае от IndexedDB, а мрежовият четец не поддържа IndexedDB в режим на поверително разглеждане.<br/><br/>Въпреки това, няма смисъл да използвате интернет-приложението ntfy в режим на поверително разглеждане, тъй като всичко се пази в хранилището на четеца. Можете да прочетете повече по <githubLink>проблема в GitHub</githubLink> или да се свържете с нас в <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,5 +152,40 @@
|
|||||||
"prefs_users_description": "Zde můžete přidávat/odebírat uživatele pro chráněná témata. Upozorňujeme, že uživatelské jméno a heslo jsou uloženy v místním úložišti prohlížeče.",
|
"prefs_users_description": "Zde můžete přidávat/odebírat uživatele pro chráněná témata. Upozorňujeme, že uživatelské jméno a heslo jsou uloženy v místním úložišti prohlížeče.",
|
||||||
"error_boundary_gathering_info": "Získejte více informací …",
|
"error_boundary_gathering_info": "Získejte více informací …",
|
||||||
"prefs_appearance_language_title": "Jazyk",
|
"prefs_appearance_language_title": "Jazyk",
|
||||||
"prefs_appearance_title": "Vzhled"
|
"prefs_appearance_title": "Vzhled",
|
||||||
|
"action_bar_show_menu": "Zobrazit nabídku",
|
||||||
|
"action_bar_logo_alt": "logo ntfy",
|
||||||
|
"action_bar_toggle_mute": "Ztlumení/zrušení ztlumení oznámení",
|
||||||
|
"action_bar_toggle_action_menu": "Otevřít/zavřít nabídku akcí",
|
||||||
|
"message_bar_show_dialog": "Zobrazit okno pro odesílání oznámení",
|
||||||
|
"message_bar_publish": "Odeslat zprávu",
|
||||||
|
"nav_button_muted": "Oznámení ztlumena",
|
||||||
|
"nav_button_connecting": "připojování",
|
||||||
|
"notifications_list": "Seznam oznámení",
|
||||||
|
"notifications_list_item": "Oznámení",
|
||||||
|
"notifications_mark_read": "Označit jako přečtené",
|
||||||
|
"notifications_delete": "Smazat",
|
||||||
|
"notifications_new_indicator": "Nové oznámení",
|
||||||
|
"notifications_attachment_image": "Obrázek přílohy",
|
||||||
|
"notifications_attachment_file_image": "soubor s obrázkem",
|
||||||
|
"notifications_attachment_file_video": "video soubor",
|
||||||
|
"notifications_attachment_file_audio": "zvukový soubor",
|
||||||
|
"notifications_attachment_file_app": "Soubor s aplikací pro Android",
|
||||||
|
"publish_dialog_emoji_picker_show": "Vybrat emoji",
|
||||||
|
"publish_dialog_topic_reset": "Obnovení tématu",
|
||||||
|
"publish_dialog_click_reset": "Odebrat URL kliknutím",
|
||||||
|
"publish_dialog_email_reset": "Odebrat přeposlání e-mailu",
|
||||||
|
"publish_dialog_attach_reset": "Odebrat URL přílohy",
|
||||||
|
"publish_dialog_attached_file_remove": "Odebrat přiložený soubor",
|
||||||
|
"emoji_picker_search_clear": "Vyčistit vyhledávání",
|
||||||
|
"prefs_users_edit_button": "Upravit uživatele",
|
||||||
|
"prefs_users_delete_button": "Odstranit uživatele",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Soukromé prohlížení není podporováno",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "Webová aplikace ntfy potřebuje ke svému fungování databázi IndexedDB a váš prohlížeč v režimu soukromého prohlížení databázi IndexedDB nepodporuje.<br/><br/>To je sice nepříjemné, ale používat webovou aplikaci ntfy v režimu soukromého prohlížení stejně nemá smysl, protože vše je uloženo v úložišti prohlížeče. Více se o tom můžete dočíst <githubLink>v tomto tématu na GitHubu</githubLink>, nebo se na nás obrátit pomocí služeb <discordLink>Discord</discordLink> nebo <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"notifications_priority_x": "Priorita {{priority}}",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL služby",
|
||||||
|
"prefs_notifications_sound_play": "Přehrát vybraný zvuk",
|
||||||
|
"prefs_users_table": "Tabulka uživatelů",
|
||||||
|
"notifications_attachment_file_document": "jiný dokument",
|
||||||
|
"publish_dialog_delay_reset": "Odebrat odložené doručení"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,5 +152,40 @@
|
|||||||
"prefs_notifications_delete_after_one_week_description": "Benachrichtigungen werden nach einer Woche automatisch gelöscht",
|
"prefs_notifications_delete_after_one_week_description": "Benachrichtigungen werden nach einer Woche automatisch gelöscht",
|
||||||
"priority_min": "min",
|
"priority_min": "min",
|
||||||
"notifications_actions_not_supported": "Diese Aktion wird in der Web-App nicht unterstützt",
|
"notifications_actions_not_supported": "Diese Aktion wird in der Web-App nicht unterstützt",
|
||||||
"notifications_actions_http_request_title": "Sende HTTP {{method}} an {{url}}"
|
"notifications_actions_http_request_title": "Sende HTTP {{method}} an {{url}}",
|
||||||
|
"action_bar_show_menu": "Menü anzeigen",
|
||||||
|
"action_bar_toggle_mute": "Stummschaltung der Benachrichtigungen an/aus",
|
||||||
|
"message_bar_show_dialog": "Dialog zur Veröffentlichung anzeigen",
|
||||||
|
"message_bar_publish": "Benachrichtigung veröffentlichen",
|
||||||
|
"nav_button_connecting": "verbinde",
|
||||||
|
"notifications_list": "Benachrichtigungsliste",
|
||||||
|
"notifications_mark_read": "Als gelesen markieren",
|
||||||
|
"notifications_delete": "Löschen",
|
||||||
|
"notifications_priority_x": "Priorität {{priority}}",
|
||||||
|
"notifications_attachment_file_image": "Bilddatei",
|
||||||
|
"notifications_attachment_image": "Bild des Anhangs",
|
||||||
|
"notifications_attachment_file_video": "Videodatei",
|
||||||
|
"notifications_attachment_file_audio": "Audiodatei",
|
||||||
|
"notifications_attachment_file_app": "Android App-Datei",
|
||||||
|
"notifications_attachment_file_document": "anderes Dokument",
|
||||||
|
"publish_dialog_attached_file_remove": "Angehängte Datei entfernen",
|
||||||
|
"emoji_picker_search_clear": "Suche leeren",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "Service URL",
|
||||||
|
"prefs_notifications_sound_play": "Gewählten Sound abspielen",
|
||||||
|
"prefs_users_table": "Benutzertabelle",
|
||||||
|
"prefs_users_edit_button": "Benutzer bearbeiten",
|
||||||
|
"prefs_users_delete_button": "Benutzer löschen",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Private Browser-Tabs werden nicht unterstützt",
|
||||||
|
"publish_dialog_delay_reset": "Verzögerte Zustellung entfernen",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "Die ntfy Web-App benötigt eine IndexedDB für eine korrekte Funktion, und Dein Browser unterstützt in privaten Tabs keinen IndexedDB.<br/><br/>Das ist zwar ärgerlich, eine Nutzung von ntfy in einem privaten Tab macht aber auch wenig Sinn da alle Daten im Browser gespeichert werden. Weitere Informationen gibt es <githubLink>in diesem GitHub-Issue</githubLink>, oder im Chat bei <discordLink>Discord</discordLink> oder <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"action_bar_toggle_action_menu": "Aktionsmenü öffnen/schließen",
|
||||||
|
"notifications_new_indicator": "Neue Benachrichtigung",
|
||||||
|
"publish_dialog_email_reset": "Email-Weiterleitung entfernen",
|
||||||
|
"action_bar_logo_alt": "ntfy Logo",
|
||||||
|
"nav_button_muted": "Benachrichtigungen stummgeschaltet",
|
||||||
|
"notifications_list_item": "Benachrichtigung",
|
||||||
|
"publish_dialog_emoji_picker_show": "Emoji wählen",
|
||||||
|
"publish_dialog_topic_reset": "Thema zurücksetzen",
|
||||||
|
"publish_dialog_attach_reset": "angehängte URL entfernen",
|
||||||
|
"publish_dialog_click_reset": "Klick-URL entfernen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"alert_not_supported_description": "Notifications are not supported in your browser.",
|
"alert_not_supported_description": "Notifications are not supported in your browser.",
|
||||||
"notifications_list": "Notifications list",
|
"notifications_list": "Notifications list",
|
||||||
"notifications_list_item": "Notification",
|
"notifications_list_item": "Notification",
|
||||||
"notifications_delete": "Delete notification",
|
"notifications_mark_read": "Mark as read",
|
||||||
|
"notifications_delete": "Delete",
|
||||||
"notifications_copied_to_clipboard": "Copied to clipboard",
|
"notifications_copied_to_clipboard": "Copied to clipboard",
|
||||||
"notifications_tags": "Tags",
|
"notifications_tags": "Tags",
|
||||||
"notifications_priority_x": "Priority {{priority}}",
|
"notifications_priority_x": "Priority {{priority}}",
|
||||||
|
|||||||
@@ -152,5 +152,40 @@
|
|||||||
"prefs_notifications_delete_after_one_week_description": "Las notificaciones se eliminan automáticamente después de una semana",
|
"prefs_notifications_delete_after_one_week_description": "Las notificaciones se eliminan automáticamente después de una semana",
|
||||||
"priority_low": "baja",
|
"priority_low": "baja",
|
||||||
"notifications_actions_not_supported": "Acción no soportada en la aplicación web",
|
"notifications_actions_not_supported": "Acción no soportada en la aplicación web",
|
||||||
"notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}"
|
"notifications_actions_http_request_title": "Enviar HTTP {{method}} a {{url}}",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "La aplicación web ntfy necesita IndexedDB para funcionar y su navegador no soporta IndexedDB en modo de navegación privada. <br/> <br/> Si bien esto es desafortunado, tampoco tiene mucho sentido usar la aplicación web ntfy en modo de navegación privada de todos modos, porque todo está almacenado en el almacenamiento del navegador. Puede leer más sobre esto <githubLink>en este issue de GitHub</githubLink>, o hablar con nosotros en <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"action_bar_show_menu": "Mostrar menú",
|
||||||
|
"action_bar_logo_alt": "logo de ntfy",
|
||||||
|
"action_bar_toggle_action_menu": "Abrir/cerrar el menú de acción",
|
||||||
|
"message_bar_show_dialog": "Mostrar diálogo de publicación",
|
||||||
|
"message_bar_publish": "Publicar mensaje",
|
||||||
|
"nav_button_muted": "Notificaciones silenciadas",
|
||||||
|
"nav_button_connecting": "conectando",
|
||||||
|
"notifications_list": "Lista de notificaciones",
|
||||||
|
"notifications_list_item": "Notificación",
|
||||||
|
"notifications_mark_read": "Marcar como leído",
|
||||||
|
"notifications_delete": "Eliminar",
|
||||||
|
"notifications_priority_x": "Prioridad {{priority}}",
|
||||||
|
"notifications_new_indicator": "Nueva notificación",
|
||||||
|
"notifications_attachment_image": "Imagen adjunta",
|
||||||
|
"notifications_attachment_file_image": "archivo de imagen",
|
||||||
|
"notifications_attachment_file_video": "archivo de video",
|
||||||
|
"notifications_attachment_file_audio": "archivo de audio",
|
||||||
|
"notifications_attachment_file_app": "Archivo de aplicación de Android",
|
||||||
|
"notifications_attachment_file_document": "otro documento",
|
||||||
|
"action_bar_toggle_mute": "Silenciar/reactivar notificaciones",
|
||||||
|
"publish_dialog_emoji_picker_show": "Elige un emoji",
|
||||||
|
"publish_dialog_topic_reset": "Restablecer tópico",
|
||||||
|
"publish_dialog_click_reset": "Eliminar URL de clic",
|
||||||
|
"publish_dialog_email_reset": "Eliminar el reenvío de correo electrónico",
|
||||||
|
"publish_dialog_attach_reset": "Eliminar la URL del archivo adjunto",
|
||||||
|
"publish_dialog_delay_reset": "Eliminar entrega retrasada",
|
||||||
|
"publish_dialog_attached_file_remove": "Eliminar el archivo adjunto",
|
||||||
|
"emoji_picker_search_clear": "Limpiar búsqueda",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL del servicio",
|
||||||
|
"prefs_notifications_sound_play": "Reproducir el sonido seleccionado",
|
||||||
|
"prefs_users_table": "Tabla de usuarios",
|
||||||
|
"prefs_users_edit_button": "Editar usuario",
|
||||||
|
"prefs_users_delete_button": "Eliminar usuario",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Navegación privada no soportada"
|
||||||
}
|
}
|
||||||
|
|||||||
156
web/public/static/langs/hu.json
Normal file
156
web/public/static/langs/hu.json
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"action_bar_send_test_notification": "Teszt értesítés küldése",
|
||||||
|
"action_bar_clear_notifications": "Összes értesítés törlése",
|
||||||
|
"alert_not_supported_description": "A böngésző nem támogatja az értesítések fogadását.",
|
||||||
|
"action_bar_settings": "Beállítások",
|
||||||
|
"action_bar_unsubscribe": "Leiratkozás",
|
||||||
|
"message_bar_type_message": "Írd ide az üzenetet",
|
||||||
|
"message_bar_error_publishing": "Hiba történt az értesítés elküldése közben",
|
||||||
|
"nav_button_all_notifications": "Összes értesítés",
|
||||||
|
"nav_topics_title": "Feliratkozott témák",
|
||||||
|
"alert_grant_title": "Az értesítések le vannak tiltva",
|
||||||
|
"alert_grant_description": "Engedélyezd a böngészőnek, hogy asztali értesítéseket jeleníttessen meg.",
|
||||||
|
"nav_button_settings": "Beállítások",
|
||||||
|
"nav_button_documentation": "Dokumentáció",
|
||||||
|
"nav_button_publish_message": "Értesítés küldése",
|
||||||
|
"alert_grant_button": "Engedélyezés",
|
||||||
|
"alert_not_supported_title": "Nem támogatott funkció",
|
||||||
|
"notifications_copied_to_clipboard": "Másolva a vágólapra",
|
||||||
|
"notifications_tags": "Címkék",
|
||||||
|
"notifications_attachment_copy_url_title": "Másolja vágólapra a csatolmány URL-ét",
|
||||||
|
"notifications_attachment_copy_url_button": "URL másolása",
|
||||||
|
"notifications_attachment_open_title": "Menjen a(z) {{url}} címre",
|
||||||
|
"notifications_attachment_open_button": "Csatolmány megnyitása",
|
||||||
|
"notifications_attachment_link_expired": "A letöltési hivatkozás lejárt",
|
||||||
|
"notifications_attachment_link_expires": "A hivatkozás {{date}}-kor jár le",
|
||||||
|
"nav_button_subscribe": "Feliratkozás témára",
|
||||||
|
"notifications_click_copy_url_title": "Másolja vágólapra a hivatkozás URL-ét",
|
||||||
|
"notifications_actions_open_url_title": "Menjen a(z) {{url}} címre",
|
||||||
|
"notifications_actions_not_supported": "A művelet nem támogatott a webes alkalmazásban",
|
||||||
|
"notifications_actions_http_request_title": "Küldjön HTTP {{method}} kérést a(z) {{url}} címre",
|
||||||
|
"notifications_none_for_topic_title": "Még nem érkezett értesítés erre a témára.",
|
||||||
|
"notifications_none_for_any_title": "Még nem érkezett egy értesítés sem.",
|
||||||
|
"notifications_none_for_any_description": "Értesítés beküldéséhez csak küldj egy PUT, vagy POST kérést a téma URL-ére. Itt egy példa az egyik témádhoz.",
|
||||||
|
"notifications_no_subscriptions_title": "Úgy tűnik, még nem iratkoztál fel egy témára sem.",
|
||||||
|
"publish_dialog_message_published": "Értesítés elküldve",
|
||||||
|
"notifications_example": "Példa",
|
||||||
|
"notifications_no_subscriptions_description": "Kattints a \"{{linktext}}\" linkre egy téma létrehozásához, vagy rá feliratkozáshoz. Ezután PUT, vagy POST kéréssel fogsz tudni értesítéseket küldeni rá, amik utána meg fognak itt jelenni.",
|
||||||
|
"publish_dialog_priority_low": "Alacsony prioritás",
|
||||||
|
"publish_dialog_priority_default": "Közepes prioritás",
|
||||||
|
"publish_dialog_priority_high": "Magas prioritás",
|
||||||
|
"notifications_more_details": "További információkért keresd fel a <websiteLink>weboldalunkat</websiteLink> vagy olvasd el a <docsLink>dokumentációt</docsLink>.",
|
||||||
|
"publish_dialog_title_no_topic": "Értesítés küldése",
|
||||||
|
"publish_dialog_attachment_limits_file_and_quota_reached": "túllépi a fájlméret korlátot ({{fileSizeLimit}}) és a kvótát is ({{remainingBytes}} maradt)",
|
||||||
|
"publish_dialog_attachment_limits_quota_reached": "túllépi a kvótát, {{remainingBytes}} maradt",
|
||||||
|
"publish_dialog_priority_min": "Legkisebb prioritás",
|
||||||
|
"publish_dialog_base_url_label": "A szolgáltatás URL-e",
|
||||||
|
"publish_dialog_base_url_placeholder": "A szolgáltatás URL-e, pl: https://example.com",
|
||||||
|
"publish_dialog_topic_label": "Téma neve",
|
||||||
|
"publish_dialog_priority_max": "Legmagasabb prioritás",
|
||||||
|
"publish_dialog_topic_placeholder": "Téma neve, pl: jozsi_riasztasai",
|
||||||
|
"publish_dialog_title_label": "Cím",
|
||||||
|
"publish_dialog_title_placeholder": "Értesítés címe, pl: Fogy a szabad hely",
|
||||||
|
"publish_dialog_message_label": "Üzenet",
|
||||||
|
"publish_dialog_message_placeholder": "Írj ide egy üzenetet",
|
||||||
|
"publish_dialog_tags_label": "Címkék",
|
||||||
|
"publish_dialog_tags_placeholder": "Címkék vesszővel elválasztva, pl: fontos,srv1-backup",
|
||||||
|
"publish_dialog_priority_label": "Prioritás",
|
||||||
|
"publish_dialog_click_label": "URL",
|
||||||
|
"publish_dialog_click_placeholder": "Webcím, ami megnyílik, ha az értesítésre kattintanak",
|
||||||
|
"publish_dialog_email_label": "Email",
|
||||||
|
"publish_dialog_email_placeholder": "Email cím, amire továbbítjuk az értesítést, pl: jozsi@example.com",
|
||||||
|
"publish_dialog_attach_label": "Csatolmány URL-e",
|
||||||
|
"publish_dialog_filename_label": "Fájlnév",
|
||||||
|
"publish_dialog_filename_placeholder": "Csatolmány fájlneve",
|
||||||
|
"publish_dialog_delay_label": "Késleltetés",
|
||||||
|
"publish_dialog_delay_placeholder": "Késleltetett küldés, pl: {{unixTimestamp}}, {{relativeTime}}, vagy \"{{naturalLanguage}}\" (Csak angolul)",
|
||||||
|
"publish_dialog_other_features": "Egyéb lehetőségek:",
|
||||||
|
"publish_dialog_chip_click_label": "Kattintási URL",
|
||||||
|
"publish_dialog_chip_attach_file_label": "Helyi fájl csatolása",
|
||||||
|
"publish_dialog_chip_delay_label": "Késleltetett kézbesítés",
|
||||||
|
"publish_dialog_chip_topic_label": "Téma megváltoztatása",
|
||||||
|
"publish_dialog_button_cancel_sending": "Küldés megállítása",
|
||||||
|
"publish_dialog_button_cancel": "Mégsem",
|
||||||
|
"publish_dialog_checkbox_publish_another": "Küldök még egyet",
|
||||||
|
"publish_dialog_attached_file_title": "Csatolt fájl:",
|
||||||
|
"publish_dialog_attached_file_filename_placeholder": "Csatolmány fájlneve",
|
||||||
|
"publish_dialog_drop_file_here": "Ejtsd ide a fájlt",
|
||||||
|
"emoji_picker_search_placeholder": "Emoji keresése",
|
||||||
|
"publish_dialog_details_examples_description": "Példákért és az összes küldési képesség részletes leírásához olvasd el a <docsLink>dokumentációt</docsLink>.",
|
||||||
|
"subscribe_dialog_subscribe_use_another_label": "Használjon másik szervert",
|
||||||
|
"subscribe_dialog_subscribe_button_subscribe": "Feliratkozás",
|
||||||
|
"subscribe_dialog_login_title": "Be kell jelentkezni",
|
||||||
|
"subscribe_dialog_subscribe_description": "A témák nem mindig vannak jelszóval védve, ezért olyan nevet válassz, ami nehezen található ki. Miután feliratkoztál, küldhetsz értesítéseket.",
|
||||||
|
"subscribe_dialog_login_description": "Ez a téma jelszóval védett. Jelentkezz be a feliratkozáshoz.",
|
||||||
|
"subscribe_dialog_login_username_label": "Felhasználónév, pl: jozsi",
|
||||||
|
"subscribe_dialog_login_password_label": "Jelszó",
|
||||||
|
"subscribe_dialog_login_button_back": "Vissza",
|
||||||
|
"subscribe_dialog_login_button_login": "Belépés",
|
||||||
|
"subscribe_dialog_error_user_anonymous": "névtelen",
|
||||||
|
"subscribe_dialog_error_user_not_authorized": "A(z) {{username}} felhasználónak nincs hozzáférése",
|
||||||
|
"prefs_notifications_min_priority_description_any": "Minden értesítést mutat, prioritástól függetlenül",
|
||||||
|
"prefs_notifications_min_priority_description_max": "Csak az 5-ös (legmagasabb) prioritású értesítések jelennek meg",
|
||||||
|
"prefs_notifications_min_priority_any": "Bármilyen prioritás",
|
||||||
|
"prefs_notifications_min_priority_low_and_higher": "Alacsony prioritás, vagy magasabb",
|
||||||
|
"prefs_notifications_min_priority_high_and_higher": "Magas, vagy legmagasabb prioritás",
|
||||||
|
"prefs_notifications_min_priority_max_only": "Csak a legmagasabb prioritás",
|
||||||
|
"prefs_notifications_sound_title": "Értesítés hangja",
|
||||||
|
"prefs_notifications_sound_description_none": "Az értesítések nem fognak hangot adni, amikor megérkeznek",
|
||||||
|
"prefs_notifications_sound_no_sound": "Hang nélkül",
|
||||||
|
"prefs_notifications_delete_after_one_week": "1 hét után",
|
||||||
|
"prefs_notifications_delete_after_one_month": "1 hónap után",
|
||||||
|
"prefs_notifications_delete_after_never_description": "Az értesítések soha nem lesznek automatikusan törölve",
|
||||||
|
"prefs_notifications_delete_after_three_hours_description": "A 3 óránál régebbi értesítések automatikus törlése",
|
||||||
|
"prefs_notifications_delete_after_one_day_description": "Az egy napnál régebbi értesítések automatikus törlése",
|
||||||
|
"prefs_users_description": "Itt tudsz hozzáadni/eltávolítani felhasználókat a védett témákról. Fontos, hogy a felhasználónevet és a jelszót a böngésző helyi tárolójába fogjuk menteni.",
|
||||||
|
"prefs_users_table_user_header": "Felhasználó",
|
||||||
|
"prefs_users_table_base_url_header": "Szerver címe",
|
||||||
|
"prefs_users_dialog_title_edit": "Felhasználó szerkesztése",
|
||||||
|
"prefs_users_dialog_username_label": "Felhasználónév, pl: jozsi",
|
||||||
|
"prefs_users_dialog_password_label": "Jelszó",
|
||||||
|
"prefs_users_dialog_button_add": "Hozzáadás",
|
||||||
|
"prefs_users_dialog_base_url_label": "Szerver címe, pl: https://ntfy.sh",
|
||||||
|
"notifications_loading": "Értesítések betöltése …",
|
||||||
|
"publish_dialog_progress_uploading": "Feltöltés …",
|
||||||
|
"notifications_click_copy_url_button": "Hivatkozás másolása",
|
||||||
|
"notifications_click_open_button": "Hivatkozás megnyitása",
|
||||||
|
"publish_dialog_progress_uploading_detail": "Feltöltés folyamatban: {{loaded}}/{{total}} ({{percent}}%) …",
|
||||||
|
"notifications_none_for_topic_description": "Értesítés beküldéséhez csak küldj egy PUT, vagy POST kérést a téma URL-ére.",
|
||||||
|
"prefs_notifications_delete_after_one_day": "1 nap után",
|
||||||
|
"publish_dialog_attach_placeholder": "Csatolandó fájl címe, pl: https://f-droid.org/F-Droid.apk",
|
||||||
|
"publish_dialog_chip_email_label": "Továbbítás email-ben",
|
||||||
|
"publish_dialog_chip_attach_url_label": "Fájl csatolása URL-lel",
|
||||||
|
"publish_dialog_button_send": "Küldés",
|
||||||
|
"subscribe_dialog_subscribe_title": "Feliratkozás témára",
|
||||||
|
"subscribe_dialog_subscribe_button_cancel": "Mégsem",
|
||||||
|
"prefs_notifications_min_priority_title": "Legkisebb megjelenítendő prioritás",
|
||||||
|
"prefs_notifications_min_priority_description_x_or_higher": "Csak akkor jelenik meg egy értesítés, ha a prioritása {{number}} ({{name}}), vagy fontosabb",
|
||||||
|
"prefs_notifications_min_priority_default_and_higher": "Közepes prioritás, vagy magasabb",
|
||||||
|
"prefs_notifications_delete_after_one_week_description": "Az egy hétnél régebbi értesítések automatikus törlése",
|
||||||
|
"prefs_users_add_button": "Felhasználó hozzáadása",
|
||||||
|
"subscribe_dialog_subscribe_topic_placeholder": "Téma neve, pl: jozsi_riasztasai",
|
||||||
|
"prefs_notifications_title": "Értesítések",
|
||||||
|
"error_boundary_button_copy_stack_trace": "Verem nyomkövetés másolása",
|
||||||
|
"prefs_notifications_delete_after_title": "Régi értesítések törlése",
|
||||||
|
"prefs_notifications_delete_after_three_hours": "3 óra után",
|
||||||
|
"error_boundary_title": "Jaj ne, az ntfy összeomlott",
|
||||||
|
"prefs_notifications_delete_after_never": "Soha",
|
||||||
|
"prefs_notifications_delete_after_one_month_description": "Az egy hónapnál régebbi értesítések automatikus törlése",
|
||||||
|
"prefs_appearance_title": "Megjelenés",
|
||||||
|
"priority_default": "közepes",
|
||||||
|
"priority_high": "magas",
|
||||||
|
"priority_max": "legmagasabb",
|
||||||
|
"priority_min": "legkisebb",
|
||||||
|
"error_boundary_gathering_info": "Több információ…",
|
||||||
|
"publish_dialog_attachment_limits_file_reached": "túllépi a fájlméret korlátot ({{fileSizeLimit}})",
|
||||||
|
"prefs_users_title": "Felhasználók kezelése",
|
||||||
|
"prefs_users_dialog_button_cancel": "Mégsem",
|
||||||
|
"prefs_users_dialog_button_save": "Mentés",
|
||||||
|
"prefs_users_dialog_title_add": "Felhasználó hozzáadása",
|
||||||
|
"prefs_appearance_language_title": "Nyelv",
|
||||||
|
"priority_low": "alacsony",
|
||||||
|
"error_boundary_stack_trace": "Verem nyomkövetés",
|
||||||
|
"publish_dialog_title_topic": "A {{topic}} téma értesítése",
|
||||||
|
"prefs_notifications_sound_description_some": "Az értesítéseket a(z) {{sound}} hang fogja jelezni",
|
||||||
|
"error_boundary_description": "Ennek nem szabadott volna megtörténnie. Nagyon sajnáljuk.<br/>Ha van egy perced, <githubLink>jelentsd be GitHubon</githubLink>, vagy tudasd velünk <discordLink>Discordon</discordLink>, vagy <matrixLink>Matrixon</matrixLink>."
|
||||||
|
}
|
||||||
@@ -152,5 +152,40 @@
|
|||||||
"priority_default": "bawaan",
|
"priority_default": "bawaan",
|
||||||
"priority_min": "min",
|
"priority_min": "min",
|
||||||
"notifications_actions_not_supported": "Tindakan tidak didukung di aplikasi web",
|
"notifications_actions_not_supported": "Tindakan tidak didukung di aplikasi web",
|
||||||
"notifications_actions_http_request_title": "Kirim {{method}} HTTP ke {{url}}"
|
"notifications_actions_http_request_title": "Kirim {{method}} HTTP ke {{url}}",
|
||||||
|
"action_bar_show_menu": "Tampilkan menu",
|
||||||
|
"action_bar_logo_alt": "logo ntfy",
|
||||||
|
"action_bar_toggle_mute": "Bisu/suarakan notifikasi",
|
||||||
|
"action_bar_toggle_action_menu": "Buka/tutup menu tindakan",
|
||||||
|
"message_bar_show_dialog": "Tampilkan dialog publikasi",
|
||||||
|
"message_bar_publish": "Publikasikan pesan",
|
||||||
|
"nav_button_muted": "Notifikasi dibisukan",
|
||||||
|
"nav_button_connecting": "menghubungkan",
|
||||||
|
"notifications_list": "Daftar notifikasi",
|
||||||
|
"notifications_list_item": "Notifikasi",
|
||||||
|
"notifications_mark_read": "Tandai sebagai dibaca",
|
||||||
|
"notifications_delete": "Hapus",
|
||||||
|
"notifications_priority_x": "Prioritas {{priority}}",
|
||||||
|
"notifications_new_indicator": "Notifikasi baru",
|
||||||
|
"notifications_attachment_image": "Lampiran gambar",
|
||||||
|
"notifications_attachment_file_image": "file gambar",
|
||||||
|
"notifications_attachment_file_video": "file",
|
||||||
|
"notifications_attachment_file_audio": "file audio",
|
||||||
|
"notifications_attachment_file_app": "file aplikasi Android",
|
||||||
|
"notifications_attachment_file_document": "dokumen lainnya",
|
||||||
|
"publish_dialog_emoji_picker_show": "Pilih emoji",
|
||||||
|
"publish_dialog_topic_reset": "Atur ulang topik",
|
||||||
|
"publish_dialog_click_reset": "Hapus URL klik",
|
||||||
|
"publish_dialog_email_reset": "Hapus terusan email",
|
||||||
|
"publish_dialog_attach_reset": "Hapus URL lampiran",
|
||||||
|
"publish_dialog_delay_reset": "Hapus pengiriman telat",
|
||||||
|
"publish_dialog_attached_file_remove": "Hapus file yang dilampirkan",
|
||||||
|
"emoji_picker_search_clear": "Hapus pencarian",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL layanan",
|
||||||
|
"prefs_notifications_sound_play": "Mainkan suara yang dipilih",
|
||||||
|
"prefs_users_table": "Tabel pengguna",
|
||||||
|
"prefs_users_edit_button": "Edit pengguna",
|
||||||
|
"prefs_users_delete_button": "Hapus pengguna",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "Aplikasi web ntfy membutuhkan IndexedDB untuk berfungsi, dan peramban Anda tidak mendukung IndexedDB dalam mode penjelajahan pribadi.<br/><br/>Meskipun ini disayangkan, penggunaan aplikasi web ntfy juga tidak masuk akal di mode penjelajahan pribadi, karena semuanya disimpan di penyimpanan peramban. Anda dapat membaca lebih lanjut tentangnya <githubLink>di masalah GitHub ini</githubLink>, atau berbicara dengan kami di <discordLink>Discord</discordLink> atau <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Penjelajahan privat tidak didukung"
|
||||||
}
|
}
|
||||||
|
|||||||
191
web/public/static/langs/it.json
Normal file
191
web/public/static/langs/it.json
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
{
|
||||||
|
"action_bar_logo_alt": "logo ntfy",
|
||||||
|
"action_bar_settings": "Impostazioni",
|
||||||
|
"action_bar_clear_notifications": "Cancella tutte le notifiche",
|
||||||
|
"action_bar_unsubscribe": "Annulla l'iscrizione",
|
||||||
|
"action_bar_toggle_action_menu": "Apri/chiudi il menu delle azioni",
|
||||||
|
"message_bar_type_message": "Digita un messaggio qui",
|
||||||
|
"message_bar_error_publishing": "Errore durante la pubblicazione della notifica",
|
||||||
|
"message_bar_show_dialog": "Mostra la finestra di dialogo di pubblicazione",
|
||||||
|
"message_bar_publish": "Pubblica messaggio",
|
||||||
|
"nav_topics_title": "Topic a cui si è iscritti",
|
||||||
|
"nav_button_all_notifications": "Tutte le notifiche",
|
||||||
|
"nav_button_settings": "Impostazioni",
|
||||||
|
"nav_button_publish_message": "Pubblica notifica",
|
||||||
|
"nav_button_subscribe": "Iscriviti al topic",
|
||||||
|
"nav_button_muted": "Notifiche disattivate",
|
||||||
|
"nav_button_connecting": "connessione",
|
||||||
|
"alert_grant_title": "Le notifiche sono disabilitate",
|
||||||
|
"alert_grant_button": "Concedi ora",
|
||||||
|
"notifications_list": "Elenco notifiche",
|
||||||
|
"notifications_list_item": "Notifiche",
|
||||||
|
"notifications_mark_read": "Segna come letto",
|
||||||
|
"notifications_delete": "Elimina",
|
||||||
|
"notifications_copied_to_clipboard": "Copiato negli appunti",
|
||||||
|
"notifications_tags": "Tags",
|
||||||
|
"notifications_priority_x": "Priorità {{priority}}",
|
||||||
|
"notifications_new_indicator": "Nuova notifica",
|
||||||
|
"notifications_attachment_image": "Immagine allegata",
|
||||||
|
"notifications_attachment_copy_url_title": "Copia l'URL dell'allegato negli appunti",
|
||||||
|
"notifications_attachment_copy_url_button": "Copia URL",
|
||||||
|
"notifications_attachment_open_title": "Vai a {{url}}",
|
||||||
|
"notifications_attachment_open_button": "Apri allegato",
|
||||||
|
"notifications_attachment_link_expires": "Il collegamento scade il {{date}}",
|
||||||
|
"notifications_attachment_link_expired": "link per il download scaduto",
|
||||||
|
"notifications_attachment_file_image": "file immagine",
|
||||||
|
"notifications_attachment_file_video": "file video",
|
||||||
|
"action_bar_toggle_mute": "Abilita/disabilita le notifiche",
|
||||||
|
"notifications_attachment_file_document": "altro documento",
|
||||||
|
"notifications_click_copy_url_button": "Copia link",
|
||||||
|
"notifications_click_open_button": "Apri link",
|
||||||
|
"notifications_actions_open_url_title": "Vai a {{url}}",
|
||||||
|
"notifications_actions_not_supported": "Azione non supportata nell'app Web",
|
||||||
|
"notifications_none_for_topic_title": "Non hai ancora ricevuto alcuna notifica per questo topic.",
|
||||||
|
"notifications_none_for_topic_description": "Per inviare notifiche a questo argomento, è sufficiente PUT o POST all'URL del topic.",
|
||||||
|
"notifications_none_for_any_title": "Non hai ricevuto alcuna notifica.",
|
||||||
|
"notifications_no_subscriptions_title": "Sembra che tu non abbia ancora abbonamenti.",
|
||||||
|
"notifications_example": "Esempio",
|
||||||
|
"notifications_more_details": "Per ulteriori informazioni, consulta il <websiteLink>sito web</websiteLink> o <docsLink>documentazione</docsLink>.",
|
||||||
|
"notifications_loading": "Caricamento notifiche in corso…",
|
||||||
|
"publish_dialog_title_topic": "Pubblica su {{topic}}",
|
||||||
|
"publish_dialog_title_no_topic": "Pubblica notifica",
|
||||||
|
"publish_dialog_progress_uploading": "Caricamento in corso…",
|
||||||
|
"publish_dialog_progress_uploading_detail": "Caricamento {{loaded}}/{{total}} ({{percent}}%)…",
|
||||||
|
"publish_dialog_message_published": "Notifica pubblicata",
|
||||||
|
"publish_dialog_attachment_limits_file_and_quota_reached": "supera {{fileSizeLimit}} limite di file e quota, {{remainingBytes}} rimanenti",
|
||||||
|
"publish_dialog_attachment_limits_file_reached": "supera di {{fileSizeLimit}} il limite dei file",
|
||||||
|
"publish_dialog_attachment_limits_quota_reached": "supera la quota, {{remainingBytes}} rimanenti",
|
||||||
|
"publish_dialog_emoji_picker_show": "Scegli emoji",
|
||||||
|
"publish_dialog_priority_min": "Min. priorità",
|
||||||
|
"publish_dialog_priority_low": "Bassa priorità",
|
||||||
|
"publish_dialog_priority_default": "Priorità predefinita",
|
||||||
|
"publish_dialog_priority_high": "Priorità alta",
|
||||||
|
"publish_dialog_priority_max": "Max. priorità",
|
||||||
|
"publish_dialog_base_url_label": "URL del servizio",
|
||||||
|
"publish_dialog_base_url_placeholder": "URL del servizio, ad es. https://esempio.com",
|
||||||
|
"publish_dialog_topic_label": "Nome topic",
|
||||||
|
"publish_dialog_topic_placeholder": "Nome topic, ad es. avvisi_di_phil",
|
||||||
|
"publish_dialog_topic_reset": "Reset topic",
|
||||||
|
"publish_dialog_title_label": "Titolo",
|
||||||
|
"publish_dialog_title_placeholder": "Titolo della notifica, ad es. Avviso di spazio su disco",
|
||||||
|
"publish_dialog_message_label": "Messaggio",
|
||||||
|
"publish_dialog_message_placeholder": "Digita un messaggio qui",
|
||||||
|
"publish_dialog_tags_label": "Tags",
|
||||||
|
"publish_dialog_priority_label": "Priorità",
|
||||||
|
"publish_dialog_click_label": "Clicca URL",
|
||||||
|
"publish_dialog_click_reset": "Rimuovi l'URL del clic",
|
||||||
|
"publish_dialog_email_label": "Email",
|
||||||
|
"publish_dialog_email_placeholder": "Indirizzo a cui inoltrare la notifica, ad es. phil@example.com",
|
||||||
|
"publish_dialog_email_reset": "Rimuovi inoltro email",
|
||||||
|
"publish_dialog_attach_label": "URL Allegato",
|
||||||
|
"publish_dialog_attach_reset": "Rimuovi l'URL dell'allegato",
|
||||||
|
"publish_dialog_filename_label": "Nome del file",
|
||||||
|
"publish_dialog_filename_placeholder": "Nome file allegato",
|
||||||
|
"publish_dialog_delay_placeholder": "Consegna ritardata, ad es. {{unixTimestamp}}, {{relativeTime}} o \"{{naturalLanguage}}\" (solo in inglese)",
|
||||||
|
"publish_dialog_delay_reset": "Rimuovere la consegna ritardata",
|
||||||
|
"publish_dialog_other_features": "Altre funzionalità:",
|
||||||
|
"publish_dialog_chip_click_label": "Fare clic su URL",
|
||||||
|
"publish_dialog_chip_email_label": "Inoltra a e-mail",
|
||||||
|
"publish_dialog_chip_attach_url_label": "Allega il file tramite URL",
|
||||||
|
"publish_dialog_chip_attach_file_label": "Allega file locale",
|
||||||
|
"publish_dialog_chip_delay_label": "Ritardo nella consegna",
|
||||||
|
"publish_dialog_button_cancel_sending": "Annulla l'invio",
|
||||||
|
"publish_dialog_button_cancel": "Annulla",
|
||||||
|
"publish_dialog_button_send": "Invia",
|
||||||
|
"publish_dialog_checkbox_publish_another": "Pubblica un altro",
|
||||||
|
"publish_dialog_attached_file_title": "File allegato:",
|
||||||
|
"publish_dialog_attached_file_remove": "Rimuovi il file allegato",
|
||||||
|
"publish_dialog_drop_file_here": "Trascina il file qui",
|
||||||
|
"emoji_picker_search_clear": "Cancella ricerca",
|
||||||
|
"subscribe_dialog_subscribe_title": "Iscriviti al topic",
|
||||||
|
"subscribe_dialog_subscribe_topic_placeholder": "Nome dell'argomento, ad es. avvisi_di_phil",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL del servizio",
|
||||||
|
"subscribe_dialog_subscribe_button_cancel": "Annulla",
|
||||||
|
"subscribe_dialog_login_title": "Accesso richiesto",
|
||||||
|
"subscribe_dialog_login_username_label": "Nome utente, ad es. phil",
|
||||||
|
"subscribe_dialog_login_button_login": "Login",
|
||||||
|
"subscribe_dialog_error_user_anonymous": "anonimo",
|
||||||
|
"prefs_notifications_sound_title": "Suono di notifica",
|
||||||
|
"prefs_notifications_sound_description_some": "Le notifiche riproducono il suono {{sound}} quando arrivano",
|
||||||
|
"prefs_notifications_sound_no_sound": "Nessun suono",
|
||||||
|
"prefs_notifications_min_priority_description_any": "Visualizzazione di tutte le notifiche, indipendentemente dalla priorità",
|
||||||
|
"prefs_notifications_min_priority_description_max": "Mostra notifiche se la priorità è 5 (max)",
|
||||||
|
"prefs_notifications_min_priority_any": "Qualsiasi priorità",
|
||||||
|
"prefs_notifications_min_priority_low_and_higher": "Priorità bassa e superiore",
|
||||||
|
"prefs_notifications_min_priority_high_and_higher": "Priorità alta e superiore",
|
||||||
|
"prefs_notifications_min_priority_max_only": "Solo priorità massima",
|
||||||
|
"prefs_notifications_delete_after_never": "Mai",
|
||||||
|
"prefs_notifications_delete_after_three_hours": "Dopo tre ore",
|
||||||
|
"prefs_notifications_delete_after_one_day": "Dopo un giorno",
|
||||||
|
"prefs_notifications_delete_after_never_description": "Le notifiche non vengono mai eliminate automaticamente",
|
||||||
|
"prefs_notifications_delete_after_one_day_description": "Le notifiche vengono eliminate automaticamente dopo un giorno",
|
||||||
|
"prefs_notifications_delete_after_one_week_description": "Le notifiche vengono eliminate automaticamente dopo una settimana",
|
||||||
|
"prefs_notifications_delete_after_one_month_description": "Le notifiche vengono eliminate automaticamente dopo un mese",
|
||||||
|
"prefs_users_title": "Gestisci gli utenti",
|
||||||
|
"prefs_users_description": "Aggiungi/rimuovi utenti per i tuoi topic protetti qui. Tieni presente che nome utente e password sono memorizzati nella memoria locale del browser.",
|
||||||
|
"prefs_users_table": "Tabella utenti",
|
||||||
|
"prefs_users_add_button": "Aggiungi utente",
|
||||||
|
"prefs_users_edit_button": "Modifica utente",
|
||||||
|
"prefs_users_delete_button": "Elimina utente",
|
||||||
|
"prefs_users_table_user_header": "Utente",
|
||||||
|
"prefs_users_table_base_url_header": "URL del servizio",
|
||||||
|
"prefs_users_dialog_title_add": "Aggiungi utente",
|
||||||
|
"prefs_users_dialog_title_edit": "Modifica utente",
|
||||||
|
"prefs_users_dialog_base_url_label": "URL del servizio, ad es. https://ntfy.sh",
|
||||||
|
"prefs_users_dialog_username_label": "Nome utente, ad es. phil",
|
||||||
|
"prefs_users_dialog_password_label": "Password",
|
||||||
|
"prefs_users_dialog_button_cancel": "Annulla",
|
||||||
|
"prefs_users_dialog_button_add": "Aggiungere",
|
||||||
|
"prefs_users_dialog_button_save": "Salva",
|
||||||
|
"prefs_appearance_title": "Aspetto",
|
||||||
|
"prefs_appearance_language_title": "Lingua",
|
||||||
|
"priority_min": "min",
|
||||||
|
"priority_low": "basso",
|
||||||
|
"priority_default": "predefinito",
|
||||||
|
"priority_high": "alto",
|
||||||
|
"priority_max": "max",
|
||||||
|
"error_boundary_title": "Oh no, ntfy è andato in crash",
|
||||||
|
"error_boundary_description": "Questo ovviamente non dovrebbe accadere. Mi dispiace molto per questo.<br/>Se hai un minuto, per favore <githubLink>segnala su GitHub</githubLink>, o faccelo sapere tramite <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink> .",
|
||||||
|
"error_boundary_button_copy_stack_trace": "Copia traccia dello stack",
|
||||||
|
"error_boundary_stack_trace": "Traccia dello stack",
|
||||||
|
"error_boundary_gathering_info": "Raccogli più informazioni…",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Navigazione privata non supportata",
|
||||||
|
"action_bar_show_menu": "Mostra menu",
|
||||||
|
"action_bar_send_test_notification": "Inviare una notifica di prova",
|
||||||
|
"alert_not_supported_description": "Le notifiche non sono supportate nel tuo browser.",
|
||||||
|
"nav_button_documentation": "Documentazione",
|
||||||
|
"notifications_actions_http_request_title": "Invia HTTP {{method}} a {{url}}",
|
||||||
|
"alert_grant_description": "Concedi al tuo browser l'autorizzazione a visualizzare le notifiche sul desktop.",
|
||||||
|
"alert_not_supported_title": "Notifiche non supportate",
|
||||||
|
"notifications_attachment_file_app": "file app Android",
|
||||||
|
"notifications_no_subscriptions_description": "Fai clic sul link \"{{linktext}}\" per creare o iscriverti a un topic. Successivamente, puoi inviare messaggi tramite PUT o POST e riceverai le notifiche qui.",
|
||||||
|
"notifications_attachment_file_audio": "file audio",
|
||||||
|
"notifications_none_for_any_description": "Per inviare notifiche a un topic, è sufficiente PUT o POST all'URL del topic. Ecco un esempio utilizzando uno dei tuoi topic.",
|
||||||
|
"notifications_click_copy_url_title": "Copia l'URL del collegamento negli appunti",
|
||||||
|
"prefs_notifications_sound_description_none": "Le notifiche non emettono alcun suono quando arrivano",
|
||||||
|
"publish_dialog_delay_label": "Ritardo",
|
||||||
|
"publish_dialog_tags_placeholder": "Elenco di tag separato da virgole, ad es. avviso, backup-srv1",
|
||||||
|
"publish_dialog_click_placeholder": "URL che viene aperto quando si fa clic sulla notifica",
|
||||||
|
"publish_dialog_attach_placeholder": "Allega file tramite URL, ad es. https://f-droid.org/F-Droid.apk",
|
||||||
|
"publish_dialog_chip_topic_label": "Cambia topic",
|
||||||
|
"publish_dialog_details_examples_description": "Per esempi e una descrizione dettagliata di tutte le funzioni di invio, fare riferimento alla <docsLink>documentazione</docsLink>.",
|
||||||
|
"publish_dialog_attached_file_filename_placeholder": "Nome file allegato",
|
||||||
|
"emoji_picker_search_placeholder": "Cerca emoji",
|
||||||
|
"subscribe_dialog_subscribe_description": "Gli argomenti potrebbero non essere protetti da password, quindi scegli un nome che non sia facile da indovinare. Una volta iscritto, puoi inviare le notifiche tramite PUT/POST.",
|
||||||
|
"subscribe_dialog_subscribe_use_another_label": "Usa un altro server",
|
||||||
|
"subscribe_dialog_login_password_label": "Password",
|
||||||
|
"subscribe_dialog_subscribe_button_subscribe": "Iscriviti",
|
||||||
|
"prefs_notifications_sound_play": "Riproduci il suono selezionato",
|
||||||
|
"prefs_notifications_min_priority_title": "Priorità minima",
|
||||||
|
"subscribe_dialog_login_description": "Questo argomento è protetto da password. Per favore inserisci username e password per iscriverti.",
|
||||||
|
"subscribe_dialog_login_button_back": "Indietro",
|
||||||
|
"subscribe_dialog_error_user_not_authorized": "Utente {{username}} non autorizzato",
|
||||||
|
"prefs_notifications_title": "Notifiche",
|
||||||
|
"prefs_notifications_delete_after_title": "Elimina le notifiche",
|
||||||
|
"prefs_notifications_min_priority_default_and_higher": "Priorità predefinita e superiore",
|
||||||
|
"prefs_notifications_min_priority_description_x_or_higher": "Mostra le notifiche se la priorità è {{number}} ({{name}}) o superiore",
|
||||||
|
"prefs_notifications_delete_after_one_week": "Dopo una settimana",
|
||||||
|
"prefs_notifications_delete_after_one_month": "Dopo un mese",
|
||||||
|
"prefs_notifications_delete_after_three_hours_description": "Le notifiche vengono eliminate automaticamente dopo tre ore",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "L'app web ntfy ha bisogno di IndexedDB per funzionare e il tuo browser non supporta IndexedDB in modalità di navigazione privata.<br/><br/>Anche se questo è un peccato, non ha molto senso usare il web ntfy app in modalità di navigazione privata comunque, perché tutto è archiviato nella memoria del browser. Puoi leggere di più a riguardo <githubLink>in questo numero di GitHub</githubLink> o parlarci su <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>."
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"publish_dialog_message_label": "メッセージ",
|
"publish_dialog_message_label": "メッセージ",
|
||||||
"publish_dialog_email_label": "メール",
|
"publish_dialog_email_label": "メール",
|
||||||
"notifications_none_for_any_title": "まだ通知を受信していません。",
|
"notifications_none_for_any_title": "まだ通知を受信していません。",
|
||||||
"publish_dialog_priority_max": "優先度最高",
|
"publish_dialog_priority_max": "優先度 最高",
|
||||||
"publish_dialog_button_cancel_sending": "送信をキャンセル",
|
"publish_dialog_button_cancel_sending": "送信をキャンセル",
|
||||||
"publish_dialog_attach_label": "添付URL",
|
"publish_dialog_attach_label": "添付URL",
|
||||||
"notifications_none_for_any_description": "トピックに通知を送信するには、トピックURLにPUTまたはPOSTしてください。トピックのひとつを利用した例を示します。",
|
"notifications_none_for_any_description": "トピックに通知を送信するには、トピックURLにPUTまたはPOSTしてください。トピックのひとつを利用した例を示します。",
|
||||||
@@ -60,14 +60,14 @@
|
|||||||
"publish_dialog_email_placeholder": "通知を転送するアドレス, 例) phil@example.com",
|
"publish_dialog_email_placeholder": "通知を転送するアドレス, 例) phil@example.com",
|
||||||
"notifications_more_details": "詳しい情報は、<websiteLink>ウェブサイト</websiteLink> または <docsLink>ドキュメント</docsLink> を参照してください。",
|
"notifications_more_details": "詳しい情報は、<websiteLink>ウェブサイト</websiteLink> または <docsLink>ドキュメント</docsLink> を参照してください。",
|
||||||
"publish_dialog_attachment_limits_file_reached": "ファイルサイズ制限 {{fileSizeLimit}} を超えました",
|
"publish_dialog_attachment_limits_file_reached": "ファイルサイズ制限 {{fileSizeLimit}} を超えました",
|
||||||
"publish_dialog_priority_min": "優先度最低",
|
"publish_dialog_priority_min": "優先度 最低",
|
||||||
"publish_dialog_priority_low": "優先度低",
|
"publish_dialog_priority_low": "優先度 低",
|
||||||
"publish_dialog_priority_default": "優先度通常",
|
"publish_dialog_priority_default": "優先度 通常",
|
||||||
"publish_dialog_base_url_label": "サービスURL",
|
"publish_dialog_base_url_label": "サービスURL",
|
||||||
"publish_dialog_other_features": "他の機能:",
|
"publish_dialog_other_features": "他の機能:",
|
||||||
"notifications_loading": "通知を読み込み中…",
|
"notifications_loading": "通知を読み込み中…",
|
||||||
"publish_dialog_attachment_limits_quota_reached": "クォータを超過しました、残り{{remainingBytes}}",
|
"publish_dialog_attachment_limits_quota_reached": "クォータを超過しました、残り{{remainingBytes}}",
|
||||||
"publish_dialog_priority_high": "優先度高",
|
"publish_dialog_priority_high": "優先度 高",
|
||||||
"publish_dialog_topic_placeholder": "トピック名の例 phil_alerts",
|
"publish_dialog_topic_placeholder": "トピック名の例 phil_alerts",
|
||||||
"publish_dialog_title_placeholder": "通知タイトル 例: ディスクスペース警告",
|
"publish_dialog_title_placeholder": "通知タイトル 例: ディスクスペース警告",
|
||||||
"publish_dialog_message_placeholder": "メッセージ本文を入力してください",
|
"publish_dialog_message_placeholder": "メッセージ本文を入力してください",
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
"prefs_users_table_base_url_header": "サービスURL",
|
"prefs_users_table_base_url_header": "サービスURL",
|
||||||
"prefs_users_dialog_username_label": "ユーザー名, 例) phil",
|
"prefs_users_dialog_username_label": "ユーザー名, 例) phil",
|
||||||
"prefs_users_dialog_password_label": "パスワード",
|
"prefs_users_dialog_password_label": "パスワード",
|
||||||
"error_boundary_title": "ああ、ntfyがクラッシュしました",
|
"error_boundary_title": "おっと、ntfyがクラッシュしました",
|
||||||
"error_boundary_button_copy_stack_trace": "スタックトレースをコピー",
|
"error_boundary_button_copy_stack_trace": "スタックトレースをコピー",
|
||||||
"error_boundary_stack_trace": "スタックトレース",
|
"error_boundary_stack_trace": "スタックトレース",
|
||||||
"error_boundary_gathering_info": "更に情報を集める…",
|
"error_boundary_gathering_info": "更に情報を集める…",
|
||||||
@@ -150,5 +150,42 @@
|
|||||||
"priority_default": "通常",
|
"priority_default": "通常",
|
||||||
"prefs_notifications_delete_after_three_hours_description": "通知は3時間後に自動的に削除されます",
|
"prefs_notifications_delete_after_three_hours_description": "通知は3時間後に自動的に削除されます",
|
||||||
"priority_low": "低",
|
"priority_low": "低",
|
||||||
"priority_min": "最低"
|
"priority_min": "最低",
|
||||||
|
"notifications_actions_not_supported": "このアクションはWebアプリではサポートされていません",
|
||||||
|
"notifications_actions_http_request_title": "{{url}}にHTTP {{method}}を送信",
|
||||||
|
"prefs_users_edit_button": "ユーザーを編集",
|
||||||
|
"publish_dialog_attached_file_remove": "添付ファイルを削除",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "nfty webアプリは動作にIndexedDBを使用しますが、あなたのブラウザはプライベートブラウジングモード時にIndexedDBをサポートしていません。<br/><br/>これは残念なことですが、ntfy webアプリは全ての情報をブラウザストレージに保存して動作するため、プライベートブラウジングモードで利用するのはあまり意味がないかも知れません。詳細については <githubLink>GitHub issue</githubLink>を参照するか、<discordLink>Discord</discordLink>や<matrixLink>Matrix</matrixLink>の議論に参加してください。",
|
||||||
|
"action_bar_show_menu": "メニューを表示",
|
||||||
|
"action_bar_logo_alt": "ntfyロゴ",
|
||||||
|
"action_bar_toggle_mute": "通知をミュート/解除",
|
||||||
|
"action_bar_toggle_action_menu": "動作メニューを開く/閉じる",
|
||||||
|
"message_bar_show_dialog": "送信ダイアログを表示",
|
||||||
|
"message_bar_publish": "メッセージを送信",
|
||||||
|
"nav_button_muted": "ミュートされた通知",
|
||||||
|
"nav_button_connecting": "接続中",
|
||||||
|
"notifications_list": "通知一覧",
|
||||||
|
"notifications_new_indicator": "新しい通知",
|
||||||
|
"notifications_list_item": "通知",
|
||||||
|
"notifications_mark_read": "既読にする",
|
||||||
|
"notifications_delete": "削除",
|
||||||
|
"notifications_priority_x": "優先度 {{priority}}",
|
||||||
|
"notifications_attachment_image": "添付画像",
|
||||||
|
"notifications_attachment_file_image": "画像ファイル",
|
||||||
|
"notifications_attachment_file_video": "動画ファイル",
|
||||||
|
"notifications_attachment_file_audio": "音声ファイル",
|
||||||
|
"notifications_attachment_file_app": "Androidアプリファイル",
|
||||||
|
"notifications_attachment_file_document": "その他文書",
|
||||||
|
"publish_dialog_emoji_picker_show": "絵文字",
|
||||||
|
"publish_dialog_topic_reset": "トピックをリセット",
|
||||||
|
"publish_dialog_click_reset": "クリックURLを削除",
|
||||||
|
"publish_dialog_email_reset": "メール転送を削除",
|
||||||
|
"publish_dialog_attach_reset": "添付URLを削除",
|
||||||
|
"publish_dialog_delay_reset": "配信遅延を削除",
|
||||||
|
"emoji_picker_search_clear": "検索をクリア",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "サーバーURL",
|
||||||
|
"prefs_notifications_sound_play": "選択されたサウンドを再生",
|
||||||
|
"prefs_users_table": "ユーザー一覧",
|
||||||
|
"prefs_users_delete_button": "ユーザーを削除",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "プライベートブラウジングはサポートされていません"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
{}
|
{
|
||||||
|
"action_bar_settings": "Instellingen",
|
||||||
|
"action_bar_send_test_notification": "Stuur testmelding",
|
||||||
|
"action_bar_clear_notifications": "Alle meldingen wissen",
|
||||||
|
"message_bar_type_message": "Typ hier een bericht",
|
||||||
|
"action_bar_unsubscribe": "Afmelden"
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,5 +34,159 @@
|
|||||||
"notifications_attachment_link_expires": "link expira em {{date}}",
|
"notifications_attachment_link_expires": "link expira em {{date}}",
|
||||||
"notifications_attachment_copy_url_button": "Copiar URL",
|
"notifications_attachment_copy_url_button": "Copiar URL",
|
||||||
"notifications_attachment_link_expired": "link para transferência expirado",
|
"notifications_attachment_link_expired": "link para transferência expirado",
|
||||||
"notifications_example": "Exemplo"
|
"notifications_example": "Exemplo",
|
||||||
|
"notifications_more_details": "Para mais informações, confira <websiteLink>site</websiteLink> ou <docsLink>documentação</docsLink>.",
|
||||||
|
"notifications_loading": "Carregando notificações…",
|
||||||
|
"subscribe_dialog_error_user_anonymous": "anônimo",
|
||||||
|
"prefs_notifications_delete_after_three_hours": "Após três horas",
|
||||||
|
"prefs_notifications_delete_after_one_day": "Após um dia",
|
||||||
|
"prefs_notifications_delete_after_one_week": "Após uma semana",
|
||||||
|
"prefs_notifications_delete_after_one_month": "Após um mês",
|
||||||
|
"notifications_actions_not_supported": "Ação não suportada no aplicativo web",
|
||||||
|
"notifications_actions_http_request_title": "Enviar HTTP {{method}} para {{url}}",
|
||||||
|
"notifications_actions_open_url_title": "Ir para {{url}}",
|
||||||
|
"publish_dialog_title_topic": "Publicar em {{topic}}",
|
||||||
|
"publish_dialog_title_no_topic": "Publicar notificação",
|
||||||
|
"publish_dialog_progress_uploading": "Enviando …",
|
||||||
|
"publish_dialog_progress_uploading_detail": "Fazendo upload de {{loaded}}/{{total}} ({{percent}}%)…",
|
||||||
|
"publish_dialog_message_published": "Notificação publicada",
|
||||||
|
"publish_dialog_attachment_limits_file_reached": "excede o limite de arquivo {{fileSizeLimit}}",
|
||||||
|
"publish_dialog_priority_min": "Prioridade mínima",
|
||||||
|
"publish_dialog_priority_low": "Baixa prioridade",
|
||||||
|
"publish_dialog_priority_default": "Prioridade padrão",
|
||||||
|
"publish_dialog_base_url_label": "URL de serviço",
|
||||||
|
"publish_dialog_base_url_placeholder": "URL de serviço, por exemplo https://example.com",
|
||||||
|
"publish_dialog_topic_label": "Nome do tópico",
|
||||||
|
"publish_dialog_topic_placeholder": "Nome do tópico, por exemplo, phil_alerts",
|
||||||
|
"publish_dialog_title_label": "Título",
|
||||||
|
"publish_dialog_title_placeholder": "Título da notificação, por exemplo Alerta de espaço em disco",
|
||||||
|
"publish_dialog_message_label": "Mensagem",
|
||||||
|
"publish_dialog_message_placeholder": "Digite uma mensagem aqui",
|
||||||
|
"publish_dialog_tags_label": "Etiquetas",
|
||||||
|
"publish_dialog_tags_placeholder": "Lista de etiquetas, separadas por vírgula, por exemplo: srv1-backup",
|
||||||
|
"publish_dialog_priority_label": "Prioridade",
|
||||||
|
"publish_dialog_click_label": "Clique em URL",
|
||||||
|
"publish_dialog_click_placeholder": "URL que é aberto quando a notificação é clicada",
|
||||||
|
"publish_dialog_email_label": "Email",
|
||||||
|
"publish_dialog_email_placeholder": "Email para encaminhar a notificação, por exemplo phil@example.com",
|
||||||
|
"publish_dialog_filename_label": "Nome do arquivo",
|
||||||
|
"publish_dialog_filename_placeholder": "Nome do arquivo anexado",
|
||||||
|
"publish_dialog_delay_label": "Atraso",
|
||||||
|
"publish_dialog_delay_placeholder": "Atraso na entrega, por exemplo {{{unixTimestamp}}, {{relativeTime}}, ou \"{{naturalLanguage}}\" (apenas em inglês)",
|
||||||
|
"publish_dialog_other_features": "Outros recursos:",
|
||||||
|
"publish_dialog_chip_click_label": "Clique em URL",
|
||||||
|
"publish_dialog_chip_attach_file_label": "Anexar arquivo local",
|
||||||
|
"publish_dialog_chip_delay_label": "Atraso na entrega",
|
||||||
|
"publish_dialog_chip_topic_label": "Alterar tópico",
|
||||||
|
"publish_dialog_button_cancel_sending": "Cancelar o envio",
|
||||||
|
"publish_dialog_attached_file_filename_placeholder": "Nome do arquivo anexado",
|
||||||
|
"publish_dialog_drop_file_here": "Solte o arquivo aqui",
|
||||||
|
"emoji_picker_search_placeholder": "Pesquisar emoji",
|
||||||
|
"subscribe_dialog_subscribe_title": "Inscrever no tópico",
|
||||||
|
"subscribe_dialog_subscribe_use_another_label": "Usar outro servidor",
|
||||||
|
"subscribe_dialog_subscribe_description": "Os tópicos podem não ser protegidos por senha, então escolha um nome que não seja fácil de adivinhar. Uma vez inscrito, você pode PUT/POST notificações.",
|
||||||
|
"subscribe_dialog_subscribe_topic_placeholder": "Nome do tópico, por exemplo phil_alerts",
|
||||||
|
"subscribe_dialog_subscribe_button_cancel": "Cancelar",
|
||||||
|
"subscribe_dialog_subscribe_button_subscribe": "Inscrever",
|
||||||
|
"prefs_notifications_min_priority_description_max": "Mostrar notificações se prioridade for 5 (máxima)",
|
||||||
|
"prefs_notifications_min_priority_any": "Qualquer prioridade",
|
||||||
|
"prefs_notifications_min_priority_low_and_higher": "Baixa prioridade e acima",
|
||||||
|
"prefs_notifications_min_priority_default_and_higher": "Prioridade padrão e acima",
|
||||||
|
"subscribe_dialog_login_password_label": "Senha",
|
||||||
|
"subscribe_dialog_login_button_back": "Voltar",
|
||||||
|
"prefs_notifications_min_priority_high_and_higher": "Alta prioridade e acima",
|
||||||
|
"prefs_notifications_min_priority_max_only": "Apenas prioridade máxima",
|
||||||
|
"prefs_notifications_delete_after_title": "Apagar notificações",
|
||||||
|
"prefs_notifications_delete_after_never": "Nunca",
|
||||||
|
"prefs_notifications_delete_after_never_description": "Notificações nunca serão auto excluídas",
|
||||||
|
"prefs_users_description": "Adicionar/remover usuários em seus tópicos protegidos. Note que o usuário e senha são salvos no armazenamento local do navegador.",
|
||||||
|
"prefs_users_add_button": "Adicionar usuário",
|
||||||
|
"prefs_users_table_user_header": "Usuário",
|
||||||
|
"prefs_users_table_base_url_header": "URL de serviço",
|
||||||
|
"prefs_users_dialog_title_add": "Adicionar usuário",
|
||||||
|
"prefs_users_dialog_title_edit": "Editar usuário",
|
||||||
|
"prefs_users_dialog_base_url_label": "URL de serviço, exemplo https://ntfy.sh",
|
||||||
|
"prefs_users_dialog_username_label": "Usuário, por exemplo phil",
|
||||||
|
"prefs_users_dialog_password_label": "Senha",
|
||||||
|
"prefs_users_dialog_button_cancel": "Cancelar",
|
||||||
|
"prefs_users_dialog_button_add": "Adicionar",
|
||||||
|
"prefs_users_dialog_button_save": "Salvar",
|
||||||
|
"prefs_appearance_title": "Aparência",
|
||||||
|
"prefs_appearance_language_title": "LInguagem",
|
||||||
|
"priority_min": "minima",
|
||||||
|
"priority_low": "baixa",
|
||||||
|
"priority_default": "padrão",
|
||||||
|
"priority_high": "alta",
|
||||||
|
"priority_max": "máxima",
|
||||||
|
"error_boundary_title": "Ah não, ntfy parou de funcionar",
|
||||||
|
"error_boundary_gathering_info": "Coletar mais informações …",
|
||||||
|
"error_boundary_description": "Isto obviamente não deveria ter acontecido. Lamentamos muito por isto.<br/>Se tiver um minuto, por favor <githubLink> relate isto no GitHub</githubLink>, ou informe-nos através de <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>.",
|
||||||
|
"error_boundary_button_copy_stack_trace": "Copiar rastreamento de pilha",
|
||||||
|
"error_boundary_stack_trace": "Rastreamento de pilha",
|
||||||
|
"publish_dialog_attachment_limits_file_and_quota_reached": "excede {{fileSizeLimit}} limite de arquivo e cota, {{remainingBytes}} restante",
|
||||||
|
"publish_dialog_attachment_limits_quota_reached": "excede a cota, {{remainingBytes}} restantes",
|
||||||
|
"publish_dialog_priority_high": "Alta prioridade",
|
||||||
|
"publish_dialog_priority_max": "Prioridade máxima",
|
||||||
|
"publish_dialog_button_send": "Enviar",
|
||||||
|
"publish_dialog_attached_file_title": "Arquivo anexado:",
|
||||||
|
"publish_dialog_attach_label": "URL de anexo",
|
||||||
|
"publish_dialog_chip_attach_url_label": "Anexar arquivo por URL",
|
||||||
|
"publish_dialog_attach_placeholder": "Anexar arquivo por URL, por exemplo, https://f-droid.org/F-Droid.apk",
|
||||||
|
"publish_dialog_chip_email_label": "Encaminhar para email",
|
||||||
|
"publish_dialog_checkbox_publish_another": "Publicar outro",
|
||||||
|
"publish_dialog_details_examples_description": "Para obter exemplos e uma descrição detalhada de todos os recursos de envio, consulte a <docsLink>documentação</docsLink>.",
|
||||||
|
"publish_dialog_button_cancel": "Cancelar",
|
||||||
|
"prefs_notifications_delete_after_one_day_description": "Notificações são automaticamente excluídas após um dia",
|
||||||
|
"prefs_notifications_delete_after_one_month_description": "Notificações são automaticamente excluídas após um mês",
|
||||||
|
"prefs_users_title": "Gerenciar usuários",
|
||||||
|
"subscribe_dialog_error_user_not_authorized": "Usuário {{username}} não autorizado",
|
||||||
|
"prefs_notifications_title": "Notificações",
|
||||||
|
"prefs_notifications_sound_no_sound": "Sem som",
|
||||||
|
"subscribe_dialog_login_title": "Login necessário",
|
||||||
|
"prefs_notifications_sound_title": "Som de notificações",
|
||||||
|
"prefs_notifications_min_priority_title": "Mínima prioridade",
|
||||||
|
"prefs_notifications_min_priority_description_any": "Mostrando todas as notificações, independente da prioridade",
|
||||||
|
"prefs_notifications_delete_after_one_week_description": "Notificações são automaticamente excluídas após uma semana",
|
||||||
|
"subscribe_dialog_login_description": "Esse tópico é protegido por senha. Por favor digite o nome de usuário e senha para inscrever.",
|
||||||
|
"subscribe_dialog_login_username_label": "Nome, por exemplo phil",
|
||||||
|
"subscribe_dialog_login_button_login": "Login",
|
||||||
|
"prefs_notifications_sound_description_none": "Notificações não reproduzem nenhum som quando chegam",
|
||||||
|
"prefs_notifications_sound_description_some": "Notificações reproduzem som {{sound}} quando chegam",
|
||||||
|
"prefs_notifications_min_priority_description_x_or_higher": "Mostrar notificações se prioridade for {{number}} ({{name}}) ou acima",
|
||||||
|
"prefs_notifications_delete_after_three_hours_description": "Notificações são automaticamente excluídas após três horas",
|
||||||
|
"publish_dialog_attach_reset": "Remover URL do anexo",
|
||||||
|
"publish_dialog_emoji_picker_show": "Escolher emoji",
|
||||||
|
"publish_dialog_attached_file_remove": "Remover arquivo anexado",
|
||||||
|
"emoji_picker_search_clear": "Limpar",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "URL de subscrição",
|
||||||
|
"notifications_list": "Lista de notificações",
|
||||||
|
"message_bar_show_dialog": "Mostrar caixa de publicação",
|
||||||
|
"publish_dialog_topic_reset": "Resetar tópico",
|
||||||
|
"publish_dialog_delay_reset": "Remover entrega adiada da notificação",
|
||||||
|
"nav_button_connecting": "Conectando",
|
||||||
|
"publish_dialog_email_reset": "Remover encaminhar email",
|
||||||
|
"prefs_notifications_sound_play": "Reproduzir som selecionado",
|
||||||
|
"action_bar_show_menu": "Mostrar menu",
|
||||||
|
"action_bar_toggle_mute": "Habilita/Desabilita notificações",
|
||||||
|
"action_bar_toggle_action_menu": "Abrir/fechar menu de ação",
|
||||||
|
"action_bar_logo_alt": "nfty logo",
|
||||||
|
"message_bar_publish": "Publicar mensagem",
|
||||||
|
"nav_button_muted": "Notificações desabilitadas",
|
||||||
|
"notifications_list_item": "Notificação",
|
||||||
|
"notifications_mark_read": "Marcar como lido",
|
||||||
|
"notifications_delete": "Excluir",
|
||||||
|
"notifications_priority_x": "Prioridade {{priority}}",
|
||||||
|
"notifications_new_indicator": "Nova notificação",
|
||||||
|
"notifications_attachment_image": "Imagem anexada",
|
||||||
|
"notifications_attachment_file_image": "Arquivo de imagem",
|
||||||
|
"notifications_attachment_file_video": "Arquivo de vídeo",
|
||||||
|
"notifications_attachment_file_audio": "Arquivo de áudio",
|
||||||
|
"notifications_attachment_file_app": "Arquivo apk android",
|
||||||
|
"notifications_attachment_file_document": "Outros documentos",
|
||||||
|
"publish_dialog_click_reset": "Remover URL clicável",
|
||||||
|
"prefs_users_table": "Tabela de usuários",
|
||||||
|
"prefs_users_edit_button": "Editar usuário",
|
||||||
|
"prefs_users_delete_button": "Excluir usuário",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Navegação anônima não suportada",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "O ntfy web app precisa do IndexedDB para funcionar, e seu navegador não suporta IndexedDB no modo de navegação privada.<br/><br/>Embora isso seja lamentável, também não faz muito sentido usar o ntfy web app no modo de navegação privada de qualquer maneira, porque tudo é armazenado no armazenamento do navegador. Você pode ler mais sobre isso <githubLink>nesta edição do GitHub</githubLink>, ou falar conosco em <discordLink>Discord</discordLink> ou <matrixLink>Matrix</matrixLink>."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,5 +152,40 @@
|
|||||||
"prefs_notifications_delete_after_never_description": "Bildirimler asla otomatik olarak silinmez",
|
"prefs_notifications_delete_after_never_description": "Bildirimler asla otomatik olarak silinmez",
|
||||||
"priority_high": "yüksek",
|
"priority_high": "yüksek",
|
||||||
"notifications_actions_not_supported": "Eylem, web uygulamasında desteklenmiyor",
|
"notifications_actions_not_supported": "Eylem, web uygulamasında desteklenmiyor",
|
||||||
"notifications_actions_http_request_title": "{{url}} adresine HTTP {{method}} gönder"
|
"notifications_actions_http_request_title": "{{url}} adresine HTTP {{method}} gönder",
|
||||||
|
"action_bar_show_menu": "Menüyü göster",
|
||||||
|
"action_bar_logo_alt": "ntfy logosu",
|
||||||
|
"action_bar_toggle_action_menu": "Eylem menüsünü aç/kapat",
|
||||||
|
"message_bar_show_dialog": "Yayınla iletişim kutusunu göster",
|
||||||
|
"message_bar_publish": "Mesaj yayınla",
|
||||||
|
"nav_button_connecting": "bağlanıyor",
|
||||||
|
"notifications_list": "Bildirimler listesi",
|
||||||
|
"notifications_list_item": "Bildirim",
|
||||||
|
"notifications_delete": "Sil",
|
||||||
|
"notifications_attachment_image": "Ek resmi",
|
||||||
|
"notifications_attachment_file_image": "resim dosyası",
|
||||||
|
"notifications_attachment_file_video": "video dosyası",
|
||||||
|
"notifications_attachment_file_audio": "ses dosyası",
|
||||||
|
"notifications_attachment_file_app": "Android uygulama dosyası",
|
||||||
|
"notifications_attachment_file_document": "diğer belge",
|
||||||
|
"publish_dialog_emoji_picker_show": "Emoji seç",
|
||||||
|
"publish_dialog_topic_reset": "Konuyu sıfırla",
|
||||||
|
"publish_dialog_attach_reset": "Ek URL'sini kaldır",
|
||||||
|
"publish_dialog_delay_reset": "Gecikmeli teslimatı kaldır",
|
||||||
|
"publish_dialog_attached_file_remove": "Ekli dosyayı kaldır",
|
||||||
|
"emoji_picker_search_clear": "Aramayı temizle",
|
||||||
|
"subscribe_dialog_subscribe_base_url_label": "Hizmet URL'si",
|
||||||
|
"prefs_notifications_sound_play": "Seçilen sesi çal",
|
||||||
|
"error_boundary_unsupported_indexeddb_description": "ntfy web uygulamasının çalışması için IndexedDB'ye ihtiyacı var ve tarayıcınız gizli tarama modunda IndexedDB'yi desteklemiyor.<br/><br/>Bu talihsiz olsa da, ntfy web uygulamasını gizli tarama modunda kullanmak pek mantıklı değildir, çünkü her şey tarayıcı deposunda saklanır. <githubLink>Bu GitHub sorununda</githubLink> bununla ilgili daha fazla bilgi edinebilir veya <discordLink>Discord</discordLink> veya <matrixLink>Matrix</matrixLink> üzerinden bizimle konuşabilirsiniz.",
|
||||||
|
"notifications_new_indicator": "Yeni bildirim",
|
||||||
|
"action_bar_toggle_mute": "Bildirimleri sesini kapat/aç",
|
||||||
|
"publish_dialog_click_reset": "Tıklama URL'sini kaldır",
|
||||||
|
"prefs_users_table": "Kullanıcılar tablosu",
|
||||||
|
"error_boundary_unsupported_indexeddb_title": "Gizli tarama desteklenmiyor",
|
||||||
|
"nav_button_muted": "Bildirimler sessize alındı",
|
||||||
|
"notifications_mark_read": "Okundu olarak işaretle",
|
||||||
|
"notifications_priority_x": "Öncelik {{priority}}",
|
||||||
|
"publish_dialog_email_reset": "E-posta yönlendirmesini kaldır",
|
||||||
|
"prefs_users_edit_button": "Kullanıcıyı düzenle",
|
||||||
|
"prefs_users_delete_button": "Kullanıcı sil"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ class SubscriptionManager {
|
|||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markNotificationRead(notificationId) {
|
||||||
|
await db.notifications
|
||||||
|
.where({id: notificationId})
|
||||||
|
.modify({new: 0});
|
||||||
|
}
|
||||||
|
|
||||||
async markNotificationsRead(subscriptionId) {
|
async markNotificationsRead(subscriptionId) {
|
||||||
await db.notifications
|
await db.notifications
|
||||||
.where({subscriptionId: subscriptionId, new: 1})
|
.where({subscriptionId: subscriptionId, new: 1})
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
unmatchedTags
|
unmatchedTags
|
||||||
} from "../app/utils";
|
} from "../app/utils";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles";
|
import {LightboxBackdrop, Paragraph, VerticallyCenteredContainer} from "./styles";
|
||||||
import {useLiveQuery} from "dexie-react-hooks";
|
import {useLiveQuery} from "dexie-react-hooks";
|
||||||
@@ -135,6 +136,10 @@ const NotificationItem = (props) => {
|
|||||||
console.log(`[Notifications] Deleting notification ${notification.id}`);
|
console.log(`[Notifications] Deleting notification ${notification.id}`);
|
||||||
await subscriptionManager.deleteNotification(notification.id)
|
await subscriptionManager.deleteNotification(notification.id)
|
||||||
}
|
}
|
||||||
|
const handleMarkRead = async () => {
|
||||||
|
console.log(`[Notifications] Marking notification ${notification.id} as read`);
|
||||||
|
await subscriptionManager.markNotificationRead(notification.id)
|
||||||
|
}
|
||||||
const handleCopy = (s) => {
|
const handleCopy = (s) => {
|
||||||
navigator.clipboard.writeText(s);
|
navigator.clipboard.writeText(s);
|
||||||
props.onShowSnack();
|
props.onShowSnack();
|
||||||
@@ -147,9 +152,17 @@ const NotificationItem = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
|
<Card sx={{ minWidth: 275, padding: 1 }} role="listitem" aria-label={t("notifications_list_item")}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
|
<Tooltip title={t("notifications_delete")} enterDelay={500}>
|
||||||
<CloseIcon />
|
<IconButton onClick={handleDelete} sx={{ float: 'right', marginRight: -1, marginTop: -1 }} aria-label={t("notifications_delete")}>
|
||||||
</IconButton>
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{notification.new === 1 &&
|
||||||
|
<Tooltip title={t("notifications_mark_read")} enterDelay={500}>
|
||||||
|
<IconButton onClick={handleMarkRead} sx={{ float: 'right', marginRight: -0.5, marginTop: -1 }} aria-label={t("notifications_mark_read")}>
|
||||||
|
<CheckIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>}
|
||||||
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
<Typography sx={{ fontSize: 14 }} color="text.secondary">
|
||||||
{date}
|
{date}
|
||||||
{[1,2,4,5].includes(notification.priority) &&
|
{[1,2,4,5].includes(notification.priority) &&
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ const Appearance = () => {
|
|||||||
const Language = () => {
|
const Language = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const labelId = "prefLanguage";
|
const labelId = "prefLanguage";
|
||||||
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇹", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
|
||||||
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
|
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
|
||||||
const lang = i18n.language ?? "en";
|
const lang = i18n.language ?? "en";
|
||||||
|
|
||||||
@@ -449,14 +449,17 @@ const Language = () => {
|
|||||||
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
|
||||||
<Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)} aria-labelledby={labelId}>
|
<Select value={lang} onChange={(ev) => i18n.changeLanguage(ev.target.value)} aria-labelledby={labelId}>
|
||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">English</MenuItem>
|
||||||
|
<MenuItem value="id">Bahasa Indonesia</MenuItem>
|
||||||
<MenuItem value="bg">Български</MenuItem>
|
<MenuItem value="bg">Български</MenuItem>
|
||||||
<MenuItem value="cs">Čeština</MenuItem>
|
<MenuItem value="cs">Čeština</MenuItem>
|
||||||
<MenuItem value="de">Deutsch</MenuItem>
|
<MenuItem value="de">Deutsch</MenuItem>
|
||||||
<MenuItem value="es">Español</MenuItem>
|
<MenuItem value="es">Español</MenuItem>
|
||||||
<MenuItem value="fr">Français</MenuItem>
|
<MenuItem value="fr">Français</MenuItem>
|
||||||
<MenuItem value="id">Bahasa Indonesia</MenuItem>
|
<MenuItem value="it">Italiano</MenuItem>
|
||||||
|
<MenuItem value="hu">Magyar</MenuItem>
|
||||||
<MenuItem value="ja">日本語</MenuItem>
|
<MenuItem value="ja">日本語</MenuItem>
|
||||||
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
|
||||||
|
<MenuItem value="pt_BR">Português (Brasil)</MenuItem>
|
||||||
<MenuItem value="ru">Русский</MenuItem>
|
<MenuItem value="ru">Русский</MenuItem>
|
||||||
<MenuItem value="tr">Türkçe</MenuItem>
|
<MenuItem value="tr">Türkçe</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
Reference in New Issue
Block a user