From 970ca3a68ec829f909d2e93a80f0c4fb927a3d05 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 29 May 2022 20:01:05 -0400 Subject: [PATCH 01/38] Changelog --- docs/releases.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 1fdf36bc..0e0e6683 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -6,6 +6,14 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## ntfy iOS app v1.1 (UNRELEASED) +In this release of the iOS app, we add message priorities (mapped to iOS interruption levels), tags and emojis, +action buttons to open websites or perform HTTP requests (in the notification and the detail view), a custom click +action when the notification is tapped, and various other fixes. + +It also adds support for self-hosted servers (albeit not supporting auth yet). The selfhosted server needs to be +configured to forward poll requests to upstream ntfy.sh for push notifications to work (see [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) +for details). + **Features:** * [Message priority](https://ntfy.sh/docs/publish/#message-priority) support (no ticket) From 1f38a4a531df1bc36f74b40a5a3567e21bf56b18 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 29 May 2022 20:48:14 -0400 Subject: [PATCH 02/38] Upgrade Firebase Admin SDK version --- docs/releases.md | 4 ++++ go.mod | 4 +++- go.sum | 22 ++++++++++++++++++++-- server/server_firebase.go | 4 ++-- server/server_firebase_test.go | 2 +- tools/fbsend/main.go | 4 ++-- 6 files changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 0e0e6683..62f82753 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -39,6 +39,10 @@ for details). ## ntfy server v1.25.0 (UNRELEASED) +**Maintenance:** + +* Upgrade Firebase Admin SDK to 4.x ([#274](https://github.com/binwiederhier/ntfy/issues/274)) + **Documentation**: * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) diff --git a/go.mod b/go.mod index f00c1c33..3c2fed05 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.17 require ( cloud.google.com/go/firestore v1.6.1 // indirect cloud.google.com/go/storage v1.22.1 // indirect - firebase.google.com/go v3.13.0+incompatible github.com/BurntSushi/toml v1.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/emersion/go-smtp v0.15.0 @@ -26,6 +25,8 @@ require ( require github.com/pkg/errors v0.9.1 // indirect +require firebase.google.com/go/v4 v4.8.0 + require ( cloud.google.com/go v0.102.0 // indirect cloud.google.com/go/compute v1.6.1 // indirect @@ -48,6 +49,7 @@ require ( golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine/v2 v2.0.1 // indirect google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect google.golang.org/grpc v1.46.2 // indirect google.golang.org/protobuf v1.28.0 // indirect diff --git a/go.sum b/go.sum index 4a4b581a..a1881103 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,7 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0 h1:DAq3r8y4mDgyB/ZPJ9v/5VJNqjgJAxTn6ZYLlUywOu8= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= @@ -36,6 +37,7 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= @@ -45,6 +47,7 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= @@ -56,11 +59,12 @@ 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.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA= cloud.google.com/go/storage v1.22.1 h1:F6IlQJZrZM++apn9V5/VfS3gbTUYg98PS3EMQAzqtfg= 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= -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/v4 v4.8.0 h1:ooJqjFEh1G6DQ5+wyb/RAXAgku0E2RzJeH6WauSpWSo= +firebase.google.com/go/v4 v4.8.0/go.mod h1:y+j6xX7BgBco/XaN+YExIBVm6pzvYutheDV3nprvbWc= github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= @@ -425,9 +429,11 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -543,9 +549,13 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM= +google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.73.0/go.mod h1:lbd/q6BRFJbdpV6OUCXstVeiI5mL/d3/WifG7iNKnjI= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= @@ -560,6 +570,8 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine/v2 v2.0.1 h1:jTGfiRmR5qoInpT3CXJ72GJEB4owDGEKN+xRDA6ekBY= +google.golang.org/appengine/v2 v2.0.1/go.mod h1:XgltgQxPOF3ShivrVrZyfvYCx8Dunh73bKjUuXUZb8Q= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -623,8 +635,14 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= diff --git a/server/server_firebase.go b/server/server_firebase.go index 40ca806a..1facd5da 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -6,8 +6,8 @@ import ( "fmt" "strings" - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" "google.golang.org/api/option" "heckel.io/ntfy/auth" ) diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index c990b930..f3904fac 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -3,7 +3,7 @@ package server import ( "encoding/json" "errors" - "firebase.google.com/go/messaging" + "firebase.google.com/go/v4/messaging" "fmt" "github.com/stretchr/testify/require" "heckel.io/ntfy/auth" diff --git a/tools/fbsend/main.go b/tools/fbsend/main.go index cd3a06d1..832aeb79 100644 --- a/tools/fbsend/main.go +++ b/tools/fbsend/main.go @@ -2,8 +2,8 @@ package main import ( "context" - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" + firebase "firebase.google.com/go/v4" + "firebase.google.com/go/v4/messaging" "flag" "fmt" "google.golang.org/api/option" From dc0e699fb5728bdf44d2e9146e5cccf79bfaf74b Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 29 May 2022 22:14:14 -0400 Subject: [PATCH 03/38] WIP: Logging --- cmd/access.go | 4 +-- cmd/app.go | 24 +++++++++++++++ cmd/publish.go | 42 +++++++++++++------------ cmd/serve.go | 17 ++++++----- cmd/subscribe.go | 22 ++++++++------ cmd/user.go | 17 +++++------ log/log.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ server/server.go | 50 ++++++++++++++++-------------- 8 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 log/log.go diff --git a/cmd/access.go b/cmd/access.go index e70a68b7..b36dc38b 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -19,7 +19,7 @@ const ( ) var flagsAccess = append( - userCommandFlags(), + flagsUser, &cli.BoolFlag{Name: "reset", Aliases: []string{"r"}, Usage: "reset access for user (and topic)"}, ) @@ -28,7 +28,7 @@ var cmdAccess = &cli.Command{ Usage: "Grant/revoke access to a topic, or show access", UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", Flags: flagsAccess, - Before: initConfigFileInputSourceFunc("config", flagsAccess), + Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsAccess)), Action: execUserAccess, Category: categoryServer, Description: `Manage the access control list for the ntfy server. diff --git a/cmd/app.go b/cmd/app.go index 5a0b426f..89634a8b 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/urfave/cli/v2" + "heckel.io/ntfy/log" "os" ) @@ -13,6 +14,11 @@ const ( var commands = make([]*cli.Command, 0) +var flagsDefault = []cli.Flag{ + &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, + &cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}, +} + // New creates a new CLI application func New() *cli.App { return &cli.App{ @@ -25,5 +31,23 @@ func New() *cli.App { Writer: os.Stdout, ErrWriter: os.Stderr, Commands: commands, + Flags: flagsDefault, + Before: initLogFunc(nil), + } +} + +func initLogFunc(next cli.BeforeFunc) cli.BeforeFunc { + return func(c *cli.Context) error { + if c.Bool("debug") { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.ToLevel(c.String("log-level"))) + } + if next != nil { + if err := next(c); err != nil { + return err + } + } + return nil } } diff --git a/cmd/publish.go b/cmd/publish.go index 06f8d962..51d30b6a 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -16,31 +16,35 @@ func init() { commands = append(commands, cmdPublish) } +var flagsPublish = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"}, + &cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"}, + &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, + &cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"}, + &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"}, + &cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"}, + &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, + &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, + &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, + &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, + &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, + &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, + &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, + &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"}, + &cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"}, + &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"}, +) + var cmdPublish = &cli.Command{ Name: "publish", Aliases: []string{"pub", "send", "trigger"}, Usage: "Send message via a ntfy server", - UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]\n NTFY_TOPIC=.. ntfy send [OPTIONS..] -P [MESSAGE]", + UsageText: "ntfy publish [OPTIONS..] TOPIC [MESSAGE]\nNTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE]", Action: execPublish, Category: categoryClient, - Flags: []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"}, - &cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"}, - &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, - &cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"}, - &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"}, - &cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"}, - &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, - &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, - &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, - &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, - &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, - &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"}, - &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"}, - &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"}, - &cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"}, - &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do print message"}, - }, + Flags: flagsPublish, + Before: initLogFunc(nil), Description: `Publish a message to a ntfy server. Examples: diff --git a/cmd/serve.go b/cmd/serve.go index 630889d0..df1f5798 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,7 +5,7 @@ package cmd import ( "errors" "fmt" - "log" + "heckel.io/ntfy/log" "math" "net" "strings" @@ -21,7 +21,8 @@ func init() { commands = append(commands, cmdServe) } -var flagsServe = []cli.Flag{ +var flagsServe = append( + flagsDefault, &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), @@ -59,7 +60,7 @@ var flagsServe = []cli.Flag{ altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}), altsrc.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{ Name: "serve", @@ -68,7 +69,7 @@ var cmdServe = &cli.Command{ Action: execServe, Category: categoryServer, Flags: flagsServe, - Before: initConfigFileInputSourceFunc("config", flagsServe), + Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsServe)), Description: `Run the ntfy server and listen for incoming requests The command will load the configuration from /etc/ntfy/server.yml. Config options can @@ -192,7 +193,7 @@ func execServe(c *cli.Context) error { for _, host := range visitorRequestLimitExemptHosts { ips, err := net.LookupIP(host) if err != nil { - log.Printf("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error()) + log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error()) continue } for _, ip := range ips { @@ -242,12 +243,12 @@ func execServe(c *cli.Context) error { conf.EnableWeb = enableWeb s, err := server.New(conf) if err != nil { - log.Fatalln(err) + log.Fatal(err) } if err := s.Run(); err != nil { - log.Fatalln(err) + log.Fatal(err) } - log.Printf("Exiting.") + log.Info("Exiting.") return nil } diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 97f7e410..618cdb9b 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -24,6 +24,17 @@ const ( clientUserConfigFileWindowsRelative = "ntfy\\client.yml" ) +var flagsSubscribe = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, + &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"}, + &cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"}, + &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"}, + &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"}, + &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, + &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"}, +) + var cmdSubscribe = &cli.Command{ Name: "subscribe", Aliases: []string{"sub"}, @@ -31,15 +42,8 @@ var cmdSubscribe = &cli.Command{ UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]", Action: execSubscribe, Category: categoryClient, - Flags: []cli.Flag{ - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, - &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"}, - &cli.StringFlag{Name: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"}, - &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"}, - &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"}, - &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, - &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"}, - }, + Flags: flagsSubscribe, + Before: initLogFunc(nil), Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for every arriving message. There are 3 modes in which the command can be run: diff --git a/cmd/user.go b/cmd/user.go index 5ccc5b15..921aeda1 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -17,14 +17,19 @@ func init() { commands = append(commands, cmdUser) } -var flagsUser = userCommandFlags() +var flagsUser = append( + flagsDefault, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), + altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), +) var cmdUser = &cli.Command{ Name: "user", Usage: "Manage/show users", UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...", Flags: flagsUser, - Before: initConfigFileInputSourceFunc("config", flagsUser), + Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsUser)), Category: categoryServer, Subcommands: []*cli.Command{ { @@ -269,11 +274,3 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) { } return string(password), nil } - -func userCommandFlags() []cli.Flag { - return []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"}, - altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), - } -} diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..8c134508 --- /dev/null +++ b/log/log.go @@ -0,0 +1,79 @@ +package log + +import ( + "log" + "strings" +) + +type Level int + +const ( + DebugLevel Level = iota + InfoLevel + WarnLevel + ErrorLevel +) + +func (l Level) String() string { + switch l { + case DebugLevel: + return "DEBUG" + case InfoLevel: + return "INFO" + case WarnLevel: + return "WARN" + case ErrorLevel: + return "ERROR" + } + return "unknown" +} + +var ( + level = InfoLevel +) + +func Debug(message string, v ...interface{}) { + logIf(DebugLevel, message, v...) +} + +func Info(message string, v ...interface{}) { + logIf(InfoLevel, message, v...) +} + +func Warn(message string, v ...interface{}) { + logIf(WarnLevel, message, v...) +} + +func Error(message string, v ...interface{}) { + logIf(ErrorLevel, message, v...) +} + +func Fatal(v ...interface{}) { + log.Fatalln(v...) +} + +func SetLevel(newLevel Level) { + level = newLevel +} + +func ToLevel(s string) Level { + switch strings.ToLower(s) { + case "debug": + return DebugLevel + case "info": + return InfoLevel + case "warn", "warning": + return WarnLevel + case "error": + return ErrorLevel + default: + log.Fatalf("unknown log level: %s", s) + return 0 + } +} + +func logIf(l Level, message string, v ...interface{}) { + if level <= l { + log.Printf(l.String()+" "+message, v...) + } +} diff --git a/server/server.go b/server/server.go index 86ed7539..fe5a661c 100644 --- a/server/server.go +++ b/server/server.go @@ -9,8 +9,8 @@ import ( "encoding/json" "errors" "fmt" + "heckel.io/ntfy/log" "io" - "log" "net" "net/http" "net/http/httptest" @@ -181,7 +181,7 @@ func (s *Server) Run() error { if s.config.SMTPServerListen != "" { listenStr += fmt.Sprintf(" %s[smtp]", s.config.SMTPServerListen) } - log.Printf("Listening on%s", listenStr) + log.Info("Listening on%s", listenStr) mux := http.NewServeMux() mux.HandleFunc("/", s.handle) errChan := make(chan error) @@ -221,7 +221,7 @@ func (s *Server) Run() error { } s.mu.Unlock() go s.runManager() - go s.runAtSender() + go s.runDelaySender() go s.runFirebaseKeepaliver() return <-errChan @@ -248,16 +248,18 @@ func (s *Server) Stop() { func (s *Server) handle(w http.ResponseWriter, r *http.Request) { v := s.visitor(r) + log.Debug("[%s] %s %s", v.ip, r.Method, r.URL.Path) + if err := s.handleInternal(w, r, v); err != nil { if websocket.IsWebSocketUpgrade(r) { - log.Printf("[%s] WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + log.Info("[%s] WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) return // Do not attempt to write to upgraded connection } httpErr, ok := err.(*errHTTP) if !ok { httpErr = errHTTPInternalError } - log.Printf("[%s] HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + log.Info("[%s] HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.WriteHeader(httpErr.HTTPCode) @@ -434,6 +436,8 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito m.Message = emptyMessageBody } delayed := m.Time > time.Now().Unix() + log.Debug("[%s] %s %s: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", + v.ip, r.Method, r.URL.Path, m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) if !delayed { if err := t.Publish(m); err != nil { return err @@ -466,13 +470,13 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito 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()) + log.Warn("[%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()) + log.Warn("[%s] MAIL - Unable to send email: %v", v.ip, err.Error()) } } @@ -482,16 +486,16 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { 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()) + log.Warn("[%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()) + log.Warn("[%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) + log.Warn("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode) return } } @@ -1015,17 +1019,17 @@ func (s *Server) updateStatsAndPrune() { ids, err := s.messageCache.AttachmentsExpired() if err == nil { if err := s.fileCache.Remove(ids...); err != nil { - log.Printf("error while deleting attachments: %s", err.Error()) + log.Warn("Error deleting attachments: %s", err.Error()) } } else { - log.Printf("error retrieving expired attachments: %s", err.Error()) + log.Warn("Error retrieving expired attachments: %s", err.Error()) } } // Prune message cache olderThan := time.Now().Add(-1 * s.config.CacheDuration) if err := s.messageCache.Prune(olderThan); err != nil { - log.Printf("error pruning cache: %s", err.Error()) + log.Warn("Error pruning cache: %s", err.Error()) } // Prune old topics, remove subscriptions without subscribers @@ -1034,7 +1038,7 @@ func (s *Server) updateStatsAndPrune() { subs := t.Subscribers() msgs, err := s.messageCache.MessageCount(t.ID) if err != nil { - log.Printf("cannot get stats for topic %s: %s", t.ID, err.Error()) + log.Warn("Cannot get stats for topic %s: %s", t.ID, err.Error()) continue } if msgs == 0 && subs == 0 { @@ -1052,7 +1056,7 @@ func (s *Server) updateStatsAndPrune() { } // Print stats - log.Printf("Stats: %d message(s) published, %d in cache, %d successful mails, %d failed, %d topic(s) active, %d subscriber(s), %d visitor(s)", + log.Info("Stats: %d message(s) published, %d in cache, %d successful mails, %d failed, %d topic(s) active, %d subscriber(s), %d visitor(s)", s.messages, messages, mailSuccess, mailFailure, len(s.topics), subscribers, len(s.visitors)) } @@ -1096,12 +1100,12 @@ func (s *Server) runManager() { } } -func (s *Server) runAtSender() { +func (s *Server) runDelaySender() { for { select { case <-time.After(s.config.AtSenderInterval): if err := s.sendDelayedMessages(); err != nil { - log.Printf("error sending scheduled messages: %s", err.Error()) + log.Warn("error sending scheduled messages: %s", err.Error()) } case <-s.closeChan: return @@ -1117,11 +1121,11 @@ func (s *Server) runFirebaseKeepaliver() { select { case <-time.After(s.config.FirebaseKeepaliveInterval): if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil { - log.Printf("error sending Firebase keepalive message to %s: %s", firebaseControlTopic, err.Error()) + log.Info("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()) + log.Info("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error()) } case <-s.closeChan: return @@ -1140,12 +1144,12 @@ func (s *Server) sendDelayedMessages() error { t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published if ok { if err := t.Publish(m); err != nil { - log.Printf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) + log.Info("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) } } if s.firebase != nil { // Firebase subscribers may not show up in topics map if err := s.firebase(m); err != nil { - log.Printf("unable to publish to Firebase: %v", err.Error()) + log.Info("unable to publish to Firebase: %v", err.Error()) } } if err := s.messageCache.MarkPublished(m); err != nil { @@ -1252,13 +1256,13 @@ func (s *Server) withAuth(next handleFunc, perm auth.Permission) handleFunc { username, password, ok := extractUserPass(r) if ok { if user, err = s.auth.Authenticate(username, password); err != nil { - log.Printf("authentication failed: %s", err.Error()) + log.Info("authentication failed: %s", err.Error()) return errHTTPUnauthorized } } for _, t := range topics { if err := s.auth.Authorize(user, t.ID, perm); err != nil { - log.Printf("unauthorized: %s", err.Error()) + log.Info("unauthorized: %s", err.Error()) return errHTTPForbidden } } From dcfb19bfc9c4866e41cabc308979dcea0e6a54fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=81=E9=A3=9E?= Date: Mon, 30 May 2022 05:01:17 +0200 Subject: [PATCH 04/38] Added translation using Weblate (Chinese (Simplified)) --- web/public/static/langs/zh_Hans.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 web/public/static/langs/zh_Hans.json diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/web/public/static/langs/zh_Hans.json @@ -0,0 +1 @@ +{} From 717d6287c8123a8b482312496a2302a7ea5779a4 Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Mon, 30 May 2022 10:59:23 +0200 Subject: [PATCH 05/38] Add NixOS/Nix installation instructions --- docs/install.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/install.md b/docs/install.md index 6c18d56f..231b4306 100644 --- a/docs/install.md +++ b/docs/install.md @@ -176,6 +176,12 @@ cd ntfysh-bin makepkg -si ``` +## NixOS / Nix +ntfy is packaged in nixpkgs as `ntfy-sh`. It can be installed by adding the package name to the configuration file and calling `nixos-rebuild`. Alternatively, the following command can be used to install ntfy in the current user environment: +``` +nix-env -iA ntfy-sh +``` + ## 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`). From aaa004847c8fe680f8cffa5846b2879b7e44628b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=81=E9=A3=9E?= Date: Mon, 30 May 2022 05:27:28 +0000 Subject: [PATCH 06/38] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (189 of 189 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/ --- web/public/static/langs/zh_Hans.json | 192 ++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json index 0967ef42..e084bcf2 100644 --- a/web/public/static/langs/zh_Hans.json +++ b/web/public/static/langs/zh_Hans.json @@ -1 +1,191 @@ -{} +{ + "action_bar_show_menu": "显示菜单", + "action_bar_logo_alt": "ntfy图标", + "action_bar_settings": "设置", + "action_bar_send_test_notification": "发送测试通知", + "action_bar_clear_notifications": "清除所有通知", + "action_bar_unsubscribe": "取消订阅", + "action_bar_toggle_action_menu": "开启或关闭操作菜单", + "message_bar_type_message": "在此处输入消息", + "message_bar_show_dialog": "显示发布对话框", + "message_bar_publish": "发布消息", + "nav_topics_title": "订阅主题", + "nav_button_all_notifications": "全部通知", + "nav_button_documentation": "文档", + "nav_button_publish_message": "发布通知", + "nav_button_subscribe": "订阅主题", + "nav_button_connecting": "正在连接", + "alert_grant_title": "已禁用通知", + "alert_grant_description": "授予浏览器显示桌面通知的权限。", + "alert_grant_button": "现在授予", + "alert_not_supported_title": "不支持通知", + "alert_not_supported_description": "您的浏览器不支持通知。", + "notifications_list": "通知列表", + "notifications_list_item": "通知", + "notifications_mark_read": "标记为已读", + "notifications_copied_to_clipboard": "复制到剪贴板", + "notifications_tags": "标记", + "notifications_priority_x": "优先级 {{priority}}", + "notifications_new_indicator": "新通知", + "notifications_attachment_open_button": "打开附件", + "notifications_attachment_link_expires": "链接过期 {{date}}", + "notifications_attachment_link_expired": "下载链接已过期", + "notifications_attachment_file_image": "图片文件", + "notifications_attachment_image": "附件图片", + "notifications_attachment_file_video": "视频文件", + "notifications_attachment_file_audio": "音频文件", + "notifications_attachment_file_app": "安卓应用文件", + "notifications_attachment_file_document": "其他文件", + "notifications_click_copy_url_title": "复制链接地址到剪贴板", + "notifications_click_copy_url_button": "复制链接", + "notifications_click_open_button": "打开链接", + "action_bar_toggle_mute": "暂停或恢复通知", + "nav_button_muted": "已暂停通知", + "notifications_actions_not_supported": "网页应用程序不支持操作", + "notifications_none_for_topic_title": "您尚未收到有关此主题的任何通知。", + "notifications_none_for_any_title": "您尚未收到任何通知。", + "notifications_none_for_any_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。以下是使用您的主题的示例。", + "notifications_no_subscriptions_title": "看起来你还没有任何订阅。", + "notifications_example": "示例", + "notifications_more_details": "有关更多信息,请查看网站文档。", + "notifications_loading": "正在加载通知……", + "publish_dialog_title_topic": "发布到 {{topic}}", + "publish_dialog_title_no_topic": "发布通知", + "publish_dialog_progress_uploading": "正在上传……", + "publish_dialog_progress_uploading_detail": "正在上传 {{loaded}}/{{total}} ({{percent}}%) ……", + "publish_dialog_message_published": "已发布通知", + "publish_dialog_attachment_limits_file_and_quota_reached": "超过 {{fileSizeLimit}} 文件限制和配额,剩余 {{remainingBytes}}", + "publish_dialog_emoji_picker_show": "选择表情符号", + "publish_dialog_priority_min": "最低优先级", + "publish_dialog_priority_low": "低优先级", + "publish_dialog_priority_default": "默认优先级", + "publish_dialog_priority_high": "高优先级", + "publish_dialog_priority_max": "最高优先级", + "publish_dialog_topic_label": "主题名称", + "publish_dialog_topic_placeholder": "主题名称,例如 phil_alerts", + "publish_dialog_topic_reset": "重置主题", + "publish_dialog_title_label": "主题", + "publish_dialog_message_label": "消息", + "publish_dialog_message_placeholder": "在此输入消息", + "publish_dialog_tags_label": "标记", + "publish_dialog_priority_label": "优先级", + "publish_dialog_base_url_label": "服务链接地址", + "publish_dialog_base_url_placeholder": "服务链接地址,例如 https://example.com", + "publish_dialog_click_label": "点击链接地址", + "publish_dialog_click_placeholder": "点击通知时打开链接地址", + "publish_dialog_email_placeholder": "将通知转发到的地址,例如 phil@example.com", + "publish_dialog_email_reset": "移除电子邮件转发", + "publish_dialog_filename_label": "文件名", + "publish_dialog_filename_placeholder": "附件文件名", + "publish_dialog_delay_label": "延期", + "publish_dialog_other_features": "其它功能:", + "publish_dialog_attach_placeholder": "使用链接地址附加文件,例如 https://f-droid.org/F-Droid.apk", + "publish_dialog_delay_reset": "删除延迟交付", + "publish_dialog_attach_reset": "移除附件链接地址", + "publish_dialog_chip_click_label": "点击链接地址", + "publish_dialog_chip_email_label": "转发邮件", + "publish_dialog_chip_attach_file_label": "本地文件附件", + "publish_dialog_chip_topic_label": "变更主题", + "publish_dialog_button_cancel_sending": "取消发送", + "publish_dialog_checkbox_publish_another": "发布另一个", + "publish_dialog_attached_file_title": "附件文件:", + "publish_dialog_attached_file_filename_placeholder": "附件文件名", + "publish_dialog_attached_file_remove": "删除附件文件", + "publish_dialog_drop_file_here": "将文件拖拽至此", + "emoji_picker_search_placeholder": "查找表情符号", + "emoji_picker_search_clear": "清除搜索", + "subscribe_dialog_subscribe_title": "订阅主题", + "publish_dialog_chip_delay_label": "延迟交付", + "publish_dialog_chip_attach_url_label": "链接附件地址", + "subscribe_dialog_subscribe_use_another_label": "使用其他服务器", + "subscribe_dialog_subscribe_button_subscribe": "订阅", + "subscribe_dialog_login_title": "请登录", + "subscribe_dialog_login_description": "本主题受密码保护,请输入用户名和密码进行订阅。", + "subscribe_dialog_login_username_label": "用户名,例如 phil", + "subscribe_dialog_login_password_label": "密码", + "subscribe_dialog_login_button_back": "返回", + "subscribe_dialog_login_button_login": "登录", + "subscribe_dialog_error_user_not_authorized": "未授权 {{username}} 用户", + "subscribe_dialog_error_user_anonymous": "匿名", + "prefs_notifications_title": "通知", + "prefs_notifications_sound_title": "通知提示音", + "prefs_notifications_sound_description_none": "收到通知时不播放任何声音", + "prefs_notifications_sound_description_some": "收到通知时播放 {{sound}} 声音", + "prefs_notifications_sound_no_sound": "静音", + "prefs_notifications_sound_play": "播放选中声音", + "prefs_notifications_min_priority_title": "最低优先级", + "prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知", + "prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知", + "prefs_notifications_min_priority_any": "任意优先级", + "prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级", + "prefs_notifications_min_priority_default_and_higher": "默认优先级或更高优先级", + "prefs_notifications_min_priority_high_and_higher": "高优先级或更高优先级", + "prefs_notifications_min_priority_max_only": "仅最高优先级", + "prefs_notifications_delete_after_never": "从不", + "prefs_notifications_delete_after_one_month": "一月后", + "prefs_notifications_delete_after_one_week": "一周后", + "prefs_notifications_delete_after_never_description": "永不自动删除通知", + "prefs_notifications_delete_after_three_hours_description": "三小时后自动删除通知", + "prefs_notifications_delete_after_one_day_description": "一天后自动删除通知", + "prefs_notifications_delete_after_one_week_description": "一周后自动删除通知", + "prefs_notifications_delete_after_one_month_description": "一月后后自动删除通知", + "prefs_users_title": "管理用户", + "prefs_users_description": "在此处添加/删除受保护主题的用户。请注意,用户名和密码存储在浏览器的本地存储中。", + "prefs_users_add_button": "添加用户", + "prefs_users_dialog_title_add": "添加用户", + "prefs_users_dialog_title_edit": "编辑用户", + "prefs_users_dialog_username_label": "用户名,例如 phil", + "prefs_users_dialog_password_label": "密码", + "prefs_users_dialog_button_cancel": "取消", + "prefs_users_dialog_button_save": "保存", + "prefs_appearance_title": "外观", + "prefs_appearance_language_title": "语言", + "priority_min": "最低", + "priority_low": "低", + "priority_default": "默认", + "priority_high": "高", + "priority_max": "最高", + "error_boundary_title": "天啊,ntfy 崩溃了", + "prefs_users_table_base_url_header": "服务链接地址", + "prefs_users_dialog_base_url_label": "服务链接地址,例如 https://ntfy.sh", + "error_boundary_button_copy_stack_trace": "复制堆栈跟踪", + "error_boundary_stack_trace": "堆栈跟踪", + "error_boundary_gathering_info": "收集更多信息……", + "error_boundary_unsupported_indexeddb_title": "不支持隐私浏览", + "error_boundary_unsupported_indexeddb_description": "Ntfy Web应用程序需要IndexedDB才能运行,并且您的浏览器在私隐私浏览模式下不支持IndexedDB。

虽然这很不幸,但在隐私浏览模式下使用ntfy Web应用程序也没有多大意义,因为所有东西都存储在浏览器存储中。您可以在本GitHub问题中阅读有关它的更多信息,或者在DiscordMatrix上与我们交谈。", + "message_bar_error_publishing": "发布通知时出错", + "nav_button_settings": "设置", + "notifications_delete": "删除", + "notifications_attachment_copy_url_title": "将附件中链接地址复制到剪贴板", + "notifications_attachment_copy_url_button": "复制链接地址", + "notifications_attachment_open_title": "转到 {{url}}", + "notifications_actions_http_request_title": "发送 HTTP {{method}} 到 {{url}}", + "notifications_actions_open_url_title": "转到 {{url}}", + "notifications_none_for_topic_description": "要向此主题发送通知,只需使用 PUT 或 POST 到主题链接即可。", + "subscribe_dialog_subscribe_topic_placeholder": "主题名,例如 phil_alerts", + "notifications_no_subscriptions_description": "单击 \"{{linktext}}\" 链接以创建或订阅主题。之后,您可以使用 PUT 或 POST 发送消息,您将在这里收到通知。", + "publish_dialog_attachment_limits_file_reached": "超过 {{fileSizeLimit}} 文件限制", + "publish_dialog_title_placeholder": "主题标题,例如 磁盘空间告警", + "publish_dialog_email_label": "电子邮件", + "publish_dialog_button_send": "发送", + "publish_dialog_attachment_limits_quota_reached": "超过配额,剩余 {{remainingBytes}}", + "publish_dialog_attach_label": "附件链接地址", + "publish_dialog_click_reset": "移除点击连接地址", + "publish_dialog_button_cancel": "取消", + "subscribe_dialog_subscribe_button_cancel": "取消", + "subscribe_dialog_subscribe_base_url_label": "服务地址地址", + "prefs_notifications_min_priority_description_any": "显示所有通知,无论优先级如何", + "prefs_notifications_delete_after_title": "删除通知", + "prefs_notifications_delete_after_three_hours": "三小时后", + "prefs_users_delete_button": "删除用户", + "prefs_users_table_user_header": "用户", + "prefs_users_dialog_button_add": "添加", + "prefs_notifications_delete_after_one_day": "一天后", + "error_boundary_description": "这显然不应该发生。对此非常抱歉。
如果您有时间,请在GitHub上报告,或通过DiscordMatrix告诉我们。", + "prefs_users_table": "用户表", + "prefs_users_edit_button": "编辑用户", + "publish_dialog_tags_placeholder": "英文逗号分隔标记列表,例如 warning, srv1-backup", + "publish_dialog_details_examples_description": "有关所有发送功能的示例和详细说明,请参阅文档。", + "subscribe_dialog_subscribe_description": "主题可能不受密码保护,因此请选择一个不容易猜测的名字。订阅后,您可以使用 PUT/POST 通知。", + "publish_dialog_delay_placeholder": "延迟交付,例如{{unixTimestamp}}、{{relativeTime}}或“{{naturalLanguage}}”(仅限英语)" +} From 670ea67052c8158c026986da358483361ec033bb Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 11:01:36 -0400 Subject: [PATCH 07/38] Redo CI pipelines, build from GitHub Actions, closes #36 --- .github/workflows/build.yaml | 39 ++ .github/workflows/codeql-analysis.yml | 72 --- .github/workflows/release.yaml | 50 ++ .github/workflows/test.yaml | 44 +- .goreleaser.yml | 1 + Makefile | 52 +- web/package-lock.json | 716 +++++++++++++------------- 7 files changed, 510 insertions(+), 464 deletions(-) create mode 100644 .github/workflows/build.yaml delete mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..45a5ae2a --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,39 @@ +name: build +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.18.x' + - + name: Install node + uses: actions/setup-node@v2 + with: + node-version: '16' + - + name: Checkout code + uses: actions/checkout@v2 + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build all the things + run: make build + - + name: Print build results and checksums + run: make cli-build-results diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 31fdfe20..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,72 +0,0 @@ -# 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 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..a2a6e335 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,50 @@ +name: release +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' +jobs: + release: + runs-on: ubuntu-latest + steps: + - + name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.18.x' + - + name: Install node + uses: actions/setup-node@v2 + with: + node-version: '16' + - + name: Checkout code + uses: actions/checkout@v2 + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Docker login + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build and publish + run: make release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - + name: Print build results and checksums + run: make cli-build-results diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0b63387f..96c43d85 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -3,26 +3,46 @@ on: [push, pull_request] jobs: test: runs-on: ubuntu-latest - steps: - - name: Install Go + steps: + - + name: Install Go uses: actions/setup-go@v2 with: - go-version: '1.17.x' - - name: Install node + go-version: '1.18.x' + - + name: Install node uses: actions/setup-node@v2 with: node-version: '16' - - name: Checkout code + - + name: Checkout code uses: actions/checkout@v2 - - name: Install dependencies - run: sudo apt update && sudo apt install -y python3-pip curl - - name: Build docs (required for tests) + - + name: Cache Go and npm modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/go/bin + ~/.npm + web/node_modules + key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }} + restore-keys: ${{ runner.os }}-ntfy- + - + name: Install dependencies + run: make build-deps-ubuntu + - + name: Build docs (required for tests) run: make docs - - name: Build web app (required for tests) + - + name: Build web app (required for tests) run: make web - - name: Run tests, formatting, vetting and linting + - + name: Run tests, formatting, vetting and linting run: make check - - name: Run coverage + - + name: Run coverage run: make coverage - - name: Upload coverage to codecov.io + - + name: Upload coverage to codecov.io run: make coverage-upload diff --git a/.goreleaser.yml b/.goreleaser.yml index 3be24147..ea728840 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -157,6 +157,7 @@ universal_binaries: - id: ntfy_darwin_all replace: true + name_template: ntfy checksum: name_template: 'checksums.txt' snapshot: diff --git a/Makefile b/Makefile index b58e62e3..f2e741d5 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,18 @@ build: web docs cli update: web-deps-update cli-deps-update docs-deps-update docker pull alpine +# Ubuntu-specific + +build-deps-ubuntu: + sudo apt update + sudo apt install -y \ + curl \ + gcc-aarch64-linux-gnu \ + gcc-arm-linux-gnueabi \ + upx \ + jq + which pip3 || sudo apt install -y python3-pip + # Documentation docs: docs-deps docs-build @@ -114,28 +126,29 @@ web-deps: web-deps-update: cd web && npm update + # Main server/client build cli: cli-deps - goreleaser build --snapshot --rm-dist --debug + goreleaser build --snapshot --rm-dist cli-linux-amd64: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_amd64 + goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64 cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv6 + goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6 cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_armv7 + goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7 cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64 - goreleaser build --snapshot --rm-dist --debug --id ntfy_linux_arm64 + goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64 cli-windows-amd64: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_windows_amd64 + goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64 cli-darwin-all: cli-deps-static-sites - goreleaser build --snapshot --rm-dist --debug --id ntfy_darwin_all + goreleaser build --snapshot --rm-dist --id ntfy_darwin_all cli-linux-server: cli-deps-static-sites # This is a target to build the CLI (including the server) manually. @@ -177,6 +190,7 @@ cli-deps-static-sites: cli-deps-all: which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; } + go install github.com/goreleaser/goreleaser@latest cli-deps-gcc-armv6-armv7: which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; } @@ -187,6 +201,18 @@ cli-deps-gcc-arm64: cli-deps-update: go get -u go install honnef.co/go/tools/cmd/staticcheck@latest + go install golang.org/x/lint/golint@latest + go install github.com/goreleaser/goreleaser@latest + +cli-build-results: + cat dist/config.yaml + [ -f dist/artifacts.json ] && cat dist/artifacts.json | jq . || true + [ -f dist/metadata.json ] && cat dist/metadata.json | jq . || true + [ -f dist/checksums.txt ] && cat dist/checksums.txt || true + find dist -maxdepth 2 -type f \ + \( -name '*.deb' -or -name '*.rpm' -or -name '*.zip' -or -name '*.tar.gz' -or -name 'ntfy' \) \ + -and -not -path 'dist/goreleaserdocker*' \ + -exec sha256sum {} \; # Test/check targets @@ -238,13 +264,13 @@ staticcheck: .PHONY # Releasing targets -release: clean update cli-deps release-check-tags docs web check - goreleaser release --rm-dist --debug +release: clean update cli-deps release-checks docs web check + goreleaser release --rm-dist release-snapshot: clean update cli-deps docs web check - goreleaser release --snapshot --skip-publish --rm-dist --debug + goreleaser release --snapshot --skip-publish --rm-dist -release-check-tags: +release-checks: $(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-)) if ! grep -q $(LATEST_TAG) docs/install.md; then\ echo "ERROR: Must update docs/install.md with latest tag first.";\ @@ -254,6 +280,10 @@ release-check-tags: echo "ERROR: Must update docs/releases.md with latest tag first.";\ exit 1;\ fi + if [ -n "$(shell git status -s)" ]; then\ + echo "ERROR: Git repository is in an unclean state.";\ + exit 1;\ + fi # Installing targets diff --git a/web/package-lock.json b/web/package-lock.json index c12cf8a1..9853d848 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -471,9 +471,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.3.tgz", - "integrity": "sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1063,9 +1063,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.17.12.tgz", - "integrity": "sha512-jw8XW/B1i7Lqwqj2CbrViPcZijSxfguBWZP2aN59NHgxUyO/OcO1mfdCxH13QhN5LbWhPkX+f+brKGhZTiqtZQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.4.tgz", + "integrity": "sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==", "dependencies": { "@babel/helper-plugin-utils": "^7.17.12" }, @@ -1077,16 +1077,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.17.12.tgz", - "integrity": "sha512-cvO7lc7pZat6BsvH6l/EGaI8zpl8paICaoGk+7x7guvtfak/TbIf66nYmJOH13EuG0H+Xx3M+9LQDtSvZFKXKw==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.4.tgz", + "integrity": "sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==", "dependencies": { "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-environment-visitor": "^7.18.2", "@babel/helper-function-name": "^7.17.9", "@babel/helper-optimise-call-expression": "^7.16.7", "@babel/helper-plugin-utils": "^7.17.12", - "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-replace-supers": "^7.18.2", "@babel/helper-split-export-declaration": "^7.16.7", "globals": "^11.1.0" }, @@ -1276,9 +1276,9 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.0.tgz", - "integrity": "sha512-vwKpxdHnlM5tIrRt/eA0bzfbi7gUBLN08vLu38np1nZevlPySRe6yvuATJB5F/WPJ+ur4OXwpVYq9+BsxqAQuQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.4.tgz", + "integrity": "sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==", "dependencies": { "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-module-transforms": "^7.18.0", @@ -1575,9 +1575,9 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.1.tgz", - "integrity": "sha512-F+RJmL479HJmC0KeqqwEGZMg1P7kWArLGbAKfEi9yPthJyMNjF+DjxFF/halfQvq1Q9GFM4TUbYDNV8xe4Ctqg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.4.tgz", + "integrity": "sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.0", "@babel/helper-plugin-utils": "^7.17.12", @@ -1814,9 +1814,9 @@ } }, "node_modules/@babel/types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.2.tgz", - "integrity": "sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", "dependencies": { "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" @@ -2947,6 +2947,28 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", @@ -2967,9 +2989,9 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, "node_modules/@mui/base": { - "version": "5.0.0-alpha.82", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.82.tgz", - "integrity": "sha512-WUVDjCGnLXzmGxrmfW31blhucg0sRX4YddK2Falq7FlVzwdJaPgWn/xzPZmdLL0+WXon0gQVnDrq2qvggE/GMg==", + "version": "5.0.0-alpha.83", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.83.tgz", + "integrity": "sha512-/bFcjiI36R2Epf2Y3BkZOIdxrz5uMLqOU4cRai4igJ8DHTRMZDeKbOff0SdvwJNwg8r6oPUyoeOpsWkaOOX9/g==", "dependencies": { "@babel/runtime": "^7.17.2", "@emotion/is-prop-valid": "^1.1.2", @@ -2999,9 +3021,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.0.tgz", - "integrity": "sha512-ScwLxa0q5VYV70Jfc60V/9VD0b9SvIeZ0Jddx2Dt2pBUFFO9vKdrbt9LYiT+4p21Au5NdYIb2XSHj46CLN1v3g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.2.tgz", + "integrity": "sha512-fP6KUCCZZjc2rdbMSmkNmBHDskLkmP0uCox57cbVXvomU6BOPrCxnr5YXsSsQrZB8fchx7hfH0bkAgvMZ5KM0Q==", "dependencies": { "@babel/runtime": "^7.17.2" }, @@ -3024,18 +3046,18 @@ } }, "node_modules/@mui/material": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", - "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.2.tgz", + "integrity": "sha512-w/A1KG9Czf42uTyJOiRU5U1VullOz1R3xcsBvv3BtKCCWdVP+D6v/Yb8v0tJpIixMEbjeWzWGjotQBU0nd+yNA==", "dependencies": { "@babel/runtime": "^7.17.2", - "@mui/base": "5.0.0-alpha.82", - "@mui/system": "^5.8.1", + "@mui/base": "5.0.0-alpha.83", + "@mui/system": "^5.8.2", "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "@types/react-transition-group": "^4.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "hoist-non-react-statics": "^3.3.2", "prop-types": "^15.8.1", "react-is": "^17.0.2", @@ -3124,9 +3146,9 @@ } }, "node_modules/@mui/system": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.1.tgz", - "integrity": "sha512-kWJMEN62+HJb4LMRNEAZQYc++FPYsqPsU9dCL7ByLgmz/ZzRrZ8FjDi2r4j0ZeE4kaVvqBXh+RA7tLzmCKqV9w==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.2.tgz", + "integrity": "sha512-N74gDNKM+MnWvKTMmCPvCVLH4f0ZzakP1bcMDaPctrHwcyxNcEmtTGNpIiVk0Iu7vtThZAFL3DjHpINPGF7+cg==", "dependencies": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.8.0", @@ -3134,7 +3156,7 @@ "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "prop-types": "^15.8.1" }, "engines": { @@ -3967,13 +3989,13 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz", - "integrity": "sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", "dependencies": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/type-utils": "5.26.0", - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -4013,11 +4035,11 @@ } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.26.0.tgz", - "integrity": "sha512-OgUGXC/teXD8PYOkn33RSwBJPVwL0I2ipm5OHr9g9cfAhVrPC2DxQiWqaq88MNO5mbr/ZWnav3EVBpuwDreS5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.27.0.tgz", + "integrity": "sha512-ZOn342bYh19IYvkiorrqnzNoRAr91h3GiFSSfa4tlHV+R9GgR8SxCwAi8PKMyT8+pfwMxfQdNbwKsMurbF9hzg==", "dependencies": { - "@typescript-eslint/utils": "5.26.0" + "@typescript-eslint/utils": "5.27.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4031,13 +4053,13 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.26.0.tgz", - "integrity": "sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", "dependencies": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "debug": "^4.3.4" }, "engines": { @@ -4057,12 +4079,12 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz", - "integrity": "sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", "dependencies": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0" + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4073,11 +4095,11 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz", - "integrity": "sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", "dependencies": { - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -4098,9 +4120,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.26.0.tgz", - "integrity": "sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -4110,12 +4132,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz", - "integrity": "sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", "dependencies": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4150,14 +4172,14 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.26.0.tgz", - "integrity": "sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -4193,11 +4215,11 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz", - "integrity": "sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", "dependencies": { - "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/types": "5.27.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -5172,7 +5194,7 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/bonjour-service": { "version": "1.0.12", @@ -5617,7 +5639,7 @@ "node_modules/compression/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -6050,11 +6072,11 @@ } }, "node_modules/cssnano": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.9.tgz", - "integrity": "sha512-hctQHIIeDrfMjq0bQhoVmRVaSeNNOGxkvkKVOcKpJzLr09wlRrZWH4GaYudp0aszpW8wJeaO5/yBmID9n7DNCg==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.10.tgz", + "integrity": "sha512-ACpnRgDg4m6CZD/+8SgnLcGCgy6DDGdkMbOawwdvVxNietTNLe/MtWcenp6qT0PRt5wzhGl6/cjMWCdhKXC9QA==", "dependencies": { - "cssnano-preset-default": "^5.2.9", + "cssnano-preset-default": "^5.2.10", "lilconfig": "^2.0.3", "yaml": "^1.10.2" }, @@ -6070,25 +6092,25 @@ } }, "node_modules/cssnano-preset-default": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.9.tgz", - "integrity": "sha512-/4qcQcAfFEg+gnXE5NxKmYJ9JcT+8S5SDuJCLYMDN8sM/ymZ+lgLXq5+ohx/7V2brUCkgW2OaoCzOdAN0zvhGw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.10.tgz", + "integrity": "sha512-H8TJRhTjBKVOPltp9vr9El9I+IfYsOMhmXdK0LwdvwJcxYX9oWkY7ctacWusgPWAgQq1vt/WO8v+uqpfLnM7QA==", "dependencies": { "css-declaration-sorter": "^6.2.2", "cssnano-utils": "^3.1.0", "postcss-calc": "^8.2.3", "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.1", - "postcss-discard-comments": "^5.1.1", + "postcss-convert-values": "^5.1.2", + "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", "postcss-discard-empty": "^5.1.1", "postcss-discard-overridden": "^5.1.0", "postcss-merge-longhand": "^5.1.5", - "postcss-merge-rules": "^5.1.1", + "postcss-merge-rules": "^5.1.2", "postcss-minify-font-values": "^5.1.0", "postcss-minify-gradients": "^5.1.1", "postcss-minify-params": "^5.1.3", - "postcss-minify-selectors": "^5.2.0", + "postcss-minify-selectors": "^5.2.1", "postcss-normalize-charset": "^5.1.0", "postcss-normalize-display-values": "^5.1.0", "postcss-normalize-positions": "^5.1.0", @@ -6369,7 +6391,7 @@ "node_modules/detect-port-alt/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/detective": { "version": "5.2.1", @@ -6595,9 +6617,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==" + "version": "1.4.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.142.tgz", + "integrity": "sha512-ea8Q1YX0JRp4GylOmX4gFHIizi0j9GfRW4EkaHnkZp0agRCBB4ZGeCv17IEzIvBkiYVwfoKVhKZJbTfqCRdQdg==" }, "node_modules/emittery": { "version": "0.8.1", @@ -6785,7 +6807,7 @@ "node_modules/escodegen/node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -6967,7 +6989,7 @@ "node_modules/eslint-module-utils/node_modules/locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -7079,7 +7101,7 @@ "node_modules/eslint-plugin-import/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/eslint-plugin-jest": { "version": "25.7.0", @@ -7568,7 +7590,7 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", @@ -7761,7 +7783,7 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/find-cache-dir": { "version": "3.3.2", @@ -8318,7 +8340,7 @@ "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "engines": { "node": ">=4" } @@ -8399,7 +8421,7 @@ "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -8521,7 +8543,7 @@ "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" }, "node_modules/http-errors": { "version": "2.0.0", @@ -8613,9 +8635,9 @@ } }, "node_modules/i18next": { - "version": "21.8.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz", - "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==", + "version": "21.8.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.5.tgz", + "integrity": "sha512-uI5LVG10SBHLVOclr6yY1aCimmrzeZ0dwD73Sio61E8gQEwRmKI7/M8RKM084mNNy7VscKtxzSwELrso8BKv1g==", "funding": [ { "type": "individual", @@ -8680,7 +8702,7 @@ "node_modules/identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "dependencies": { "harmony-reflect": "^1.4.6" }, @@ -8741,7 +8763,7 @@ "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "engines": { "node": ">=0.8.19" } @@ -8749,7 +8771,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -8789,7 +8811,7 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-bigint": { "version": "1.0.4", @@ -8881,7 +8903,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { "node": ">=0.10.0" } @@ -8916,7 +8938,7 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, "node_modules/is-negative-zero": { "version": "2.0.2", @@ -8954,7 +8976,7 @@ "node_modules/is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "engines": { "node": ">=0.10.0" } @@ -8993,7 +9015,7 @@ "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", "engines": { "node": ">=0.10.0" } @@ -9059,7 +9081,7 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/is-weakref": { "version": "1.0.2", @@ -9086,12 +9108,12 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -11273,7 +11295,7 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.1", @@ -11349,7 +11371,7 @@ "node_modules/language-tags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", "dependencies": { "language-subtag-registry": "~0.3.2" } @@ -11430,12 +11452,12 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -11445,12 +11467,12 @@ "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -11520,7 +11542,7 @@ "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "engines": { "node": ">= 0.6" } @@ -11539,7 +11561,7 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11557,7 +11579,7 @@ "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "engines": { "node": ">= 0.6" } @@ -11742,7 +11764,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/negotiator": { "version": "0.6.3", @@ -12498,9 +12520,9 @@ } }, "node_modules/postcss-convert-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.1.tgz", - "integrity": "sha512-UjcYfl3wJJdcabGKk8lgetPvhi1Et7VDc3sYr9EyhNBeB00YD4vHgPBp+oMVoG/dDWCc6ASbmzPNV6jADTwh8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz", + "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==", "dependencies": { "browserslist": "^4.20.3", "postcss-value-parser": "^4.2.0" @@ -12570,9 +12592,9 @@ } }, "node_modules/postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "engines": { "node": "^10 || ^12 || >=14.0" }, @@ -12872,9 +12894,9 @@ } }, "node_modules/postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz", + "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==", "dependencies": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -12935,9 +12957,9 @@ } }, "node_modules/postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", "dependencies": { "postcss-selector-parser": "^6.0.5" }, @@ -14174,7 +14196,7 @@ "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "bin": { "jsesc": "bin/jsesc" } @@ -14359,9 +14381,9 @@ } }, "node_modules/rollup": { - "version": "2.75.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", - "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", + "version": "2.75.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.4.tgz", + "integrity": "sha512-JgZiJMJkKImMZJ8ZY1zU80Z2bA/TvrL/7D9qcBCrfl2bP+HUaIw0QHUroB4E3gBpFl6CRFM1YxGbuYGtdAswbQ==", "bin": { "rollup": "dist/bin/rollup" }, @@ -14599,7 +14621,7 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/send/node_modules/ms": { "version": "2.1.3", @@ -14650,7 +14672,7 @@ "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -14664,12 +14686,12 @@ "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", @@ -15382,13 +15404,13 @@ } }, "node_modules/terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", "dependencies": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "bin": { @@ -15444,40 +15466,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser/node_modules/source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/terser/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/terser/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "node_modules/terser/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -17107,9 +17095,9 @@ } }, "@babel/parser": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.3.tgz", - "integrity": "sha512-rL50YcEuHbbauAFAysNsJA4/f89fGTOBRNs9P81sniKnKAr4xULe5AecolcsKbi88xu0ByWYDj/S1AJ3FSFuSQ==" + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.17.12", @@ -17480,24 +17468,24 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.17.12.tgz", - "integrity": "sha512-jw8XW/B1i7Lqwqj2CbrViPcZijSxfguBWZP2aN59NHgxUyO/OcO1mfdCxH13QhN5LbWhPkX+f+brKGhZTiqtZQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.4.tgz", + "integrity": "sha512-+Hq10ye+jlvLEogSOtq4mKvtk7qwcUQ1f0Mrueai866C82f844Yom2cttfJdMdqRLTxWpsbfbkIkOIfovyUQXw==", "requires": { "@babel/helper-plugin-utils": "^7.17.12" } }, "@babel/plugin-transform-classes": { - "version": "7.17.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.17.12.tgz", - "integrity": "sha512-cvO7lc7pZat6BsvH6l/EGaI8zpl8paICaoGk+7x7guvtfak/TbIf66nYmJOH13EuG0H+Xx3M+9LQDtSvZFKXKw==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.4.tgz", + "integrity": "sha512-e42NSG2mlKWgxKUAD9EJJSkZxR67+wZqzNxLSpc51T8tRU5SLFHsPmgYR5yr7sdgX4u+iHA1C5VafJ6AyImV3A==", "requires": { "@babel/helper-annotate-as-pure": "^7.16.7", - "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-environment-visitor": "^7.18.2", "@babel/helper-function-name": "^7.17.9", "@babel/helper-optimise-call-expression": "^7.16.7", "@babel/helper-plugin-utils": "^7.17.12", - "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-replace-supers": "^7.18.2", "@babel/helper-split-export-declaration": "^7.16.7", "globals": "^11.1.0" } @@ -17609,9 +17597,9 @@ } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.0.tgz", - "integrity": "sha512-vwKpxdHnlM5tIrRt/eA0bzfbi7gUBLN08vLu38np1nZevlPySRe6yvuATJB5F/WPJ+ur4OXwpVYq9+BsxqAQuQ==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.4.tgz", + "integrity": "sha512-lH2UaQaHVOAeYrUUuZ8i38o76J/FnO8vu21OE+tD1MyP9lxdZoSfz+pDbWkq46GogUrdrMz3tiz/FYGB+bVThg==", "requires": { "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-module-transforms": "^7.18.0", @@ -17788,9 +17776,9 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.18.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.1.tgz", - "integrity": "sha512-F+RJmL479HJmC0KeqqwEGZMg1P7kWArLGbAKfEi9yPthJyMNjF+DjxFF/halfQvq1Q9GFM4TUbYDNV8xe4Ctqg==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.4.tgz", + "integrity": "sha512-l4vHuSLUajptpHNEOUDEGsnpl9pfRLsN1XUoDQDD/YBuXTM+v37SHGS+c6n4jdcZy96QtuUuSvZYMLSSsjH8Mw==", "requires": { "@babel/helper-create-class-features-plugin": "^7.18.0", "@babel/helper-plugin-utils": "^7.17.12", @@ -17976,9 +17964,9 @@ } }, "@babel/types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.2.tgz", - "integrity": "sha512-0On6B8A4/+mFUto5WERt3EEuG1NznDirvwca1O8UwXQHVY8g3R7OzYgxXdOfMwLO08UrpUD/2+3Bclyq+/C94Q==", + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", "requires": { "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" @@ -18768,6 +18756,27 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==" }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, "@jridgewell/sourcemap-codec": { "version": "1.4.13", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", @@ -18788,9 +18797,9 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, "@mui/base": { - "version": "5.0.0-alpha.82", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.82.tgz", - "integrity": "sha512-WUVDjCGnLXzmGxrmfW31blhucg0sRX4YddK2Falq7FlVzwdJaPgWn/xzPZmdLL0+WXon0gQVnDrq2qvggE/GMg==", + "version": "5.0.0-alpha.83", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.83.tgz", + "integrity": "sha512-/bFcjiI36R2Epf2Y3BkZOIdxrz5uMLqOU4cRai4igJ8DHTRMZDeKbOff0SdvwJNwg8r6oPUyoeOpsWkaOOX9/g==", "requires": { "@babel/runtime": "^7.17.2", "@emotion/is-prop-valid": "^1.1.2", @@ -18803,26 +18812,26 @@ } }, "@mui/icons-material": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.0.tgz", - "integrity": "sha512-ScwLxa0q5VYV70Jfc60V/9VD0b9SvIeZ0Jddx2Dt2pBUFFO9vKdrbt9LYiT+4p21Au5NdYIb2XSHj46CLN1v3g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.2.tgz", + "integrity": "sha512-fP6KUCCZZjc2rdbMSmkNmBHDskLkmP0uCox57cbVXvomU6BOPrCxnr5YXsSsQrZB8fchx7hfH0bkAgvMZ5KM0Q==", "requires": { "@babel/runtime": "^7.17.2" } }, "@mui/material": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.1.tgz", - "integrity": "sha512-Vl3BHFzOcAT5TJfvzoQUyuo/Xckn+/NSRyJ8upM4Hbz6Y1egW6P8f1RCa4FdkEfPSd5wSSYdmPfAiEh8eI4rPg==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.8.2.tgz", + "integrity": "sha512-w/A1KG9Czf42uTyJOiRU5U1VullOz1R3xcsBvv3BtKCCWdVP+D6v/Yb8v0tJpIixMEbjeWzWGjotQBU0nd+yNA==", "requires": { "@babel/runtime": "^7.17.2", - "@mui/base": "5.0.0-alpha.82", - "@mui/system": "^5.8.1", + "@mui/base": "5.0.0-alpha.83", + "@mui/system": "^5.8.2", "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "@types/react-transition-group": "^4.4.4", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "hoist-non-react-statics": "^3.3.2", "prop-types": "^15.8.1", "react-is": "^17.0.2", @@ -18850,9 +18859,9 @@ } }, "@mui/system": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.1.tgz", - "integrity": "sha512-kWJMEN62+HJb4LMRNEAZQYc++FPYsqPsU9dCL7ByLgmz/ZzRrZ8FjDi2r4j0ZeE4kaVvqBXh+RA7tLzmCKqV9w==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.8.2.tgz", + "integrity": "sha512-N74gDNKM+MnWvKTMmCPvCVLH4f0ZzakP1bcMDaPctrHwcyxNcEmtTGNpIiVk0Iu7vtThZAFL3DjHpINPGF7+cg==", "requires": { "@babel/runtime": "^7.17.2", "@mui/private-theming": "^5.8.0", @@ -18860,7 +18869,7 @@ "@mui/types": "^7.1.3", "@mui/utils": "^5.8.0", "clsx": "^1.1.1", - "csstype": "^3.0.11", + "csstype": "^3.1.0", "prop-types": "^15.8.1" } }, @@ -19473,13 +19482,13 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "@typescript-eslint/eslint-plugin": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.26.0.tgz", - "integrity": "sha512-oGCmo0PqnRZZndr+KwvvAUvD3kNE4AfyoGCwOZpoCncSh4MVD06JTE8XQa2u9u+NX5CsyZMBTEc2C72zx38eYA==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz", + "integrity": "sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ==", "requires": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/type-utils": "5.26.0", - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/type-utils": "5.27.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", @@ -19499,55 +19508,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.26.0.tgz", - "integrity": "sha512-OgUGXC/teXD8PYOkn33RSwBJPVwL0I2ipm5OHr9g9cfAhVrPC2DxQiWqaq88MNO5mbr/ZWnav3EVBpuwDreS5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.27.0.tgz", + "integrity": "sha512-ZOn342bYh19IYvkiorrqnzNoRAr91h3GiFSSfa4tlHV+R9GgR8SxCwAi8PKMyT8+pfwMxfQdNbwKsMurbF9hzg==", "requires": { - "@typescript-eslint/utils": "5.26.0" + "@typescript-eslint/utils": "5.27.0" } }, "@typescript-eslint/parser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.26.0.tgz", - "integrity": "sha512-n/IzU87ttzIdnAH5vQ4BBDnLPly7rC5VnjN3m0xBG82HK6rhRxnCb3w/GyWbNDghPd+NktJqB/wl6+YkzZ5T5Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.27.0.tgz", + "integrity": "sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA==", "requires": { - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.26.0.tgz", - "integrity": "sha512-gVzTJUESuTwiju/7NiTb4c5oqod8xt5GhMbExKsCTp6adU3mya6AGJ4Pl9xC7x2DX9UYFsjImC0mA62BCY22Iw==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz", + "integrity": "sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g==", "requires": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0" + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0" } }, "@typescript-eslint/type-utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.26.0.tgz", - "integrity": "sha512-7ccbUVWGLmcRDSA1+ADkDBl5fP87EJt0fnijsMFTVHXKGduYMgienC/i3QwoVhDADUAPoytgjbZbCOMj4TY55A==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz", + "integrity": "sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g==", "requires": { - "@typescript-eslint/utils": "5.26.0", + "@typescript-eslint/utils": "5.27.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.26.0.tgz", - "integrity": "sha512-8794JZFE1RN4XaExLWLI2oSXsVImNkl79PzTOOWt9h0UHROwJedNOD2IJyfL0NbddFllcktGIO2aOu10avQQyA==" + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.27.0.tgz", + "integrity": "sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A==" }, "@typescript-eslint/typescript-estree": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.26.0.tgz", - "integrity": "sha512-EyGpw6eQDsfD6jIqmXP3rU5oHScZ51tL/cZgFbFBvWuCwrIptl+oueUZzSmLtxFuSOQ9vDcJIs+279gnJkfd1w==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz", + "integrity": "sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ==", "requires": { - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/visitor-keys": "5.26.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/visitor-keys": "5.27.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -19566,14 +19575,14 @@ } }, "@typescript-eslint/utils": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.26.0.tgz", - "integrity": "sha512-PJFwcTq2Pt4AMOKfe3zQOdez6InIDOjUJJD3v3LyEtxHGVVRK3Vo7Dd923t/4M9hSH2q2CLvcTdxlLPjcIk3eg==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.27.0.tgz", + "integrity": "sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA==", "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.26.0", - "@typescript-eslint/types": "5.26.0", - "@typescript-eslint/typescript-estree": "5.26.0", + "@typescript-eslint/scope-manager": "5.27.0", + "@typescript-eslint/types": "5.27.0", + "@typescript-eslint/typescript-estree": "5.27.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -19595,11 +19604,11 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.26.0.tgz", - "integrity": "sha512-wei+ffqHanYDOQgg/fS6Hcar6wAWv0CUPQ3TZzOWd2BLfgP539rb49bwua8WRAs7R6kOSLn82rfEu2ro6Llt8Q==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz", + "integrity": "sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA==", "requires": { - "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/types": "5.27.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -20355,7 +20364,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -20692,7 +20701,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -20972,35 +20981,35 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" }, "cssnano": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.9.tgz", - "integrity": "sha512-hctQHIIeDrfMjq0bQhoVmRVaSeNNOGxkvkKVOcKpJzLr09wlRrZWH4GaYudp0aszpW8wJeaO5/yBmID9n7DNCg==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.10.tgz", + "integrity": "sha512-ACpnRgDg4m6CZD/+8SgnLcGCgy6DDGdkMbOawwdvVxNietTNLe/MtWcenp6qT0PRt5wzhGl6/cjMWCdhKXC9QA==", "requires": { - "cssnano-preset-default": "^5.2.9", + "cssnano-preset-default": "^5.2.10", "lilconfig": "^2.0.3", "yaml": "^1.10.2" } }, "cssnano-preset-default": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.9.tgz", - "integrity": "sha512-/4qcQcAfFEg+gnXE5NxKmYJ9JcT+8S5SDuJCLYMDN8sM/ymZ+lgLXq5+ohx/7V2brUCkgW2OaoCzOdAN0zvhGw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.10.tgz", + "integrity": "sha512-H8TJRhTjBKVOPltp9vr9El9I+IfYsOMhmXdK0LwdvwJcxYX9oWkY7ctacWusgPWAgQq1vt/WO8v+uqpfLnM7QA==", "requires": { "css-declaration-sorter": "^6.2.2", "cssnano-utils": "^3.1.0", "postcss-calc": "^8.2.3", "postcss-colormin": "^5.3.0", - "postcss-convert-values": "^5.1.1", - "postcss-discard-comments": "^5.1.1", + "postcss-convert-values": "^5.1.2", + "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", "postcss-discard-empty": "^5.1.1", "postcss-discard-overridden": "^5.1.0", "postcss-merge-longhand": "^5.1.5", - "postcss-merge-rules": "^5.1.1", + "postcss-merge-rules": "^5.1.2", "postcss-minify-font-values": "^5.1.0", "postcss-minify-gradients": "^5.1.1", "postcss-minify-params": "^5.1.3", - "postcss-minify-selectors": "^5.2.0", + "postcss-minify-selectors": "^5.2.1", "postcss-normalize-charset": "^5.1.0", "postcss-normalize-display-values": "^5.1.0", "postcss-normalize-positions": "^5.1.0", @@ -21212,7 +21221,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -21384,9 +21393,9 @@ } }, "electron-to-chromium": { - "version": "1.4.141", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.141.tgz", - "integrity": "sha512-mfBcbqc0qc6RlxrsIgLG2wCqkiPAjEezHxGTu7p3dHHFOurH4EjS9rFZndX5axC8264rI1Pcbw8uQP39oZckeA==" + "version": "1.4.142", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.142.tgz", + "integrity": "sha512-ea8Q1YX0JRp4GylOmX4gFHIizi0j9GfRW4EkaHnkZp0agRCBB4ZGeCv17IEzIvBkiYVwfoKVhKZJbTfqCRdQdg==" }, "emittery": { "version": "0.8.1", @@ -21526,7 +21535,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -21747,7 +21756,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "requires": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -21829,7 +21838,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -22102,7 +22111,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "safe-buffer": { "version": "5.2.1", @@ -22253,7 +22262,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -22631,7 +22640,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.0", @@ -22690,7 +22699,7 @@ "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "requires": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -22788,7 +22797,7 @@ "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=" + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==" }, "http-errors": { "version": "2.0.0", @@ -22854,9 +22863,9 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "i18next": { - "version": "21.8.4", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz", - "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==", + "version": "21.8.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.5.tgz", + "integrity": "sha512-uI5LVG10SBHLVOclr6yY1aCimmrzeZ0dwD73Sio61E8gQEwRmKI7/M8RKM084mNNy7VscKtxzSwELrso8BKv1g==", "requires": { "@babel/runtime": "^7.17.2" } @@ -22899,7 +22908,7 @@ "identity-obj-proxy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", - "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "requires": { "harmony-reflect": "^1.4.6" } @@ -22935,12 +22944,12 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -22974,7 +22983,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "is-bigint": { "version": "1.0.4", @@ -23030,7 +23039,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -23053,7 +23062,7 @@ "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" }, "is-negative-zero": { "version": "2.0.2", @@ -23076,7 +23085,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" }, "is-plain-obj": { "version": "3.0.0", @@ -23100,7 +23109,7 @@ "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=" + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" }, "is-root": { "version": "2.1.0", @@ -23139,7 +23148,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-weakref": { "version": "1.0.2", @@ -23160,12 +23169,12 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -24766,7 +24775,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "json5": { "version": "2.2.1", @@ -24819,7 +24828,7 @@ "language-tags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "integrity": "sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==", "requires": { "language-subtag-registry": "~0.3.2" } @@ -24879,12 +24888,12 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "lodash.merge": { "version": "4.6.2", @@ -24894,12 +24903,12 @@ "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, "loose-envify": { "version": "1.4.0", @@ -24957,7 +24966,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "memfs": { "version": "3.4.4", @@ -24970,7 +24979,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" }, "merge-stream": { "version": "2.0.0", @@ -24985,7 +24994,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { "version": "4.0.5", @@ -25112,7 +25121,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "negotiator": { "version": "0.6.3", @@ -25625,9 +25634,9 @@ } }, "postcss-convert-values": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.1.tgz", - "integrity": "sha512-UjcYfl3wJJdcabGKk8lgetPvhi1Et7VDc3sYr9EyhNBeB00YD4vHgPBp+oMVoG/dDWCc6ASbmzPNV6jADTwh8Q==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.2.tgz", + "integrity": "sha512-c6Hzc4GAv95B7suy4udszX9Zy4ETyMCgFPUDtWjdFTKH1SE9eFY/jEpHSwTH1QPuwxHpWslhckUQWbNRM4ho5g==", "requires": { "browserslist": "^4.20.3", "postcss-value-parser": "^4.2.0" @@ -25664,9 +25673,9 @@ } }, "postcss-discard-comments": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.1.tgz", - "integrity": "sha512-5JscyFmvkUxz/5/+TB3QTTT9Gi9jHkcn8dcmmuN68JQcv3aQg4y88yEHHhwFB52l/NkaJ43O0dbksGMAo49nfQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "requires": {} }, "postcss-discard-duplicates": { @@ -25832,9 +25841,9 @@ } }, "postcss-merge-rules": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.1.tgz", - "integrity": "sha512-8wv8q2cXjEuCcgpIB1Xx1pIy8/rhMPIQqYKNzEdyx37m6gpq83mQQdCxgIkFgliyEnKvdwJf/C61vN4tQDq4Ww==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.2.tgz", + "integrity": "sha512-zKMUlnw+zYCWoPN6yhPjtcEdlJaMUZ0WyVcxTAmw3lkkN/NDMRkOkiuctQEoWAOvH7twaxUUdvBWl0d4+hifRQ==", "requires": { "browserslist": "^4.16.6", "caniuse-api": "^3.0.0", @@ -25871,9 +25880,9 @@ } }, "postcss-minify-selectors": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.0.tgz", - "integrity": "sha512-vYxvHkW+iULstA+ctVNx0VoRAR4THQQRkG77o0oa4/mBS0OzGvvzLIvHDv/nNEM0crzN2WIyFU5X7wZhaUK3RA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", "requires": { "postcss-selector-parser": "^6.0.5" } @@ -26725,7 +26734,7 @@ "jsesc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" } } }, @@ -26848,9 +26857,9 @@ } }, "rollup": { - "version": "2.75.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.1.tgz", - "integrity": "sha512-zD73rq3Fanr/spmiybMqmGEvOpryj/heLqOb+lubxiXlo8azeJ/z306T2dJYuzfWZPQBS0OT++GXG6Lbd4ToKw==", + "version": "2.75.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.4.tgz", + "integrity": "sha512-JgZiJMJkKImMZJ8ZY1zU80Z2bA/TvrL/7D9qcBCrfl2bP+HUaIw0QHUroB4E3gBpFl6CRFM1YxGbuYGtdAswbQ==", "requires": { "fsevents": "~2.3.2" } @@ -27011,7 +27020,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -27060,7 +27069,7 @@ "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -27071,12 +27080,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "setprototypeof": { "version": "1.1.0", @@ -27629,13 +27638,13 @@ } }, "terser": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.13.1.tgz", - "integrity": "sha512-hn4WKOfwnwbYfe48NgrQjqNOH9jzLqRcIfbYytOXCOv46LBfWr9bDS17MQqOi+BWGD0sJK3Sj5NC/gJjiojaoA==", + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.0.tgz", + "integrity": "sha512-JC6qfIEkPBd9j1SMO3Pfn+A6w2kQV54tv+ABQLgZr7dA3k/DL/OBoYSWxzVpZev3J+bUHXfr55L8Mox7AaNo6g==", "requires": { + "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.8.0-beta.0", "source-map-support": "~0.5.20" }, "dependencies": { @@ -27643,37 +27652,6 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "source-map": { - "version": "0.8.0-beta.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", - "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", - "requires": { - "whatwg-url": "^7.0.0" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } } } }, From 8a81c8e95bf1a92356b0632249a8c8ce7e04526d Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 11:05:02 -0400 Subject: [PATCH 08/38] Update changelog --- docs/releases.md | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 62f82753..4131a89e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -4,13 +4,35 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release + + +## ntfy iOS app v1.1 +Released May 31, 2022 In this release of the iOS app, we add message priorities (mapped to iOS interruption levels), tags and emojis, action buttons to open websites or perform HTTP requests (in the notification and the detail view), a custom click action when the notification is tapped, and various other fixes. -It also adds support for self-hosted servers (albeit not supporting auth yet). The selfhosted server needs to be +It also adds support for self-hosted servers (albeit not supporting auth yet). The self-hosted server needs to be configured to forward poll requests to upstream ntfy.sh for push notifications to work (see [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) for details). @@ -29,26 +51,6 @@ for details). * iOS UI not always updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)) - -## ntfy Android app v1.14.0 (UNRELEASED) - -**Additional translations:** - -* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) - - -## ntfy server v1.25.0 (UNRELEASED) - -**Maintenance:** - -* Upgrade Firebase Admin SDK to 4.x ([#274](https://github.com/binwiederhier/ntfy/issues/274)) - -**Documentation**: - -* [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) - ---> - ## ntfy server v1.24.0 Released May 28, 2022 From 8283b6be975e07a9a38c58ca51728ae68647d4b0 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 20:38:56 -0400 Subject: [PATCH 09/38] Firebase quota limit --- server/config.go | 21 +++++---- server/errors.go | 1 + server/server.go | 97 ++++++++++++++++++-------------------- server/server_firebase.go | 11 ++++- server/server_test.go | 3 +- server/smtp_server.go | 42 +++++++++++++---- server/smtp_server_test.go | 97 ++++++++++++++++++++++---------------- server/topic.go | 6 +-- server/visitor.go | 21 +++++++-- 9 files changed, 180 insertions(+), 119 deletions(-) diff --git a/server/config.go b/server/config.go index 4db52a33..3de4bd70 100644 --- a/server/config.go +++ b/server/config.go @@ -6,15 +6,16 @@ import ( // Defines default config settings (excluding limits, see below) const ( - DefaultListenHTTP = ":80" - DefaultCacheDuration = 12 * time.Hour - DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) - DefaultManagerInterval = time.Minute - DefaultAtSenderInterval = 10 * time.Second - DefaultMinDelay = 10 * time.Second - DefaultMaxDelay = 3 * 24 * time.Hour - 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) + DefaultListenHTTP = ":80" + DefaultCacheDuration = 12 * time.Hour + DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) + DefaultManagerInterval = time.Minute + DefaultAtSenderInterval = 10 * time.Second + DefaultMinDelay = 10 * time.Second + DefaultMaxDelay = 3 * 24 * time.Hour + 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) + DefaultFirebaseQuotaLimitPenaltyDuration = 10 * time.Minute ) // Defines all global and per-visitor limits @@ -69,6 +70,7 @@ type Config struct { AtSenderInterval time.Duration FirebaseKeepaliveInterval time.Duration FirebasePollInterval time.Duration + FirebaseQuotaLimitPenaltyDuration time.Duration UpstreamBaseURL string SMTPSenderAddr string SMTPSenderUser string @@ -121,6 +123,7 @@ func NewConfig() *Config { AtSenderInterval: DefaultAtSenderInterval, FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, FirebasePollInterval: DefaultFirebasePollInterval, + FirebaseQuotaLimitPenaltyDuration: DefaultFirebaseQuotaLimitPenaltyDuration, TotalTopicLimit: DefaultTotalTopicLimit, VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit, VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit, diff --git a/server/errors.go b/server/errors.go index 32c1b3b9..2fa883fa 100644 --- a/server/errors.go +++ b/server/errors.go @@ -59,6 +59,7 @@ var ( errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} + errHTTPTooManyRequestsFirebaseQuotaReached = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: Firebase quota for topic reached", "https://ntfy.sh/docs/publish/#limitations"} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""} ) diff --git a/server/server.go b/server/server.go index 86ed7539..2baa3666 100644 --- a/server/server.go +++ b/server/server.go @@ -7,13 +7,11 @@ import ( "embed" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "log" "net" "net/http" - "net/http/httptest" "net/url" "os" "path" @@ -221,7 +219,7 @@ func (s *Server) Run() error { } s.mu.Unlock() go s.runManager() - go s.runAtSender() + go s.runDelayedSender() go s.runFirebaseKeepaliver() return <-errChan @@ -435,7 +433,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } delayed := m.Time > time.Now().Unix() if !delayed { - if err := t.Publish(m); err != nil { + if err := t.Publish(v, m); err != nil { return err } } @@ -465,7 +463,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) sendToFirebase(v *visitor, m *message) { - if err := s.firebase(m); err != nil { + if err := s.firebase(v, m); err != nil { log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error()) } } @@ -731,7 +729,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * return err } var wlock sync.Mutex - sub := func(msg *message) error { + sub := func(v *visitor, msg *message) error { if !filters.Pass(msg) { return nil } @@ -752,7 +750,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.Header().Set("Content-Type", contentType+"; charset=utf-8") // Android/Volley client needs charset! if poll { - return s.sendOldMessages(topics, since, scheduled, sub) + return s.sendOldMessages(topics, since, scheduled, v, sub) } subscriberIDs := make([]int, 0) for _, t := range topics { @@ -763,10 +761,10 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * topics[i].Unsubscribe(subscriberID) // Order! } }() - if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message + if err := sub(v, newOpenMessage(topicsStr)); err != nil { // Send out open message return err } - if err := s.sendOldMessages(topics, since, scheduled, sub); err != nil { + if err := s.sendOldMessages(topics, since, scheduled, v, sub); err != nil { return err } for { @@ -775,7 +773,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * return nil case <-time.After(s.config.KeepaliveInterval): v.Keepalive() - if err := sub(newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message + if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message return err } } @@ -849,7 +847,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi } } }) - sub := func(msg *message) error { + sub := func(v *visitor, msg *message) error { if !filters.Pass(msg) { return nil } @@ -862,7 +860,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi } w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests if poll { - return s.sendOldMessages(topics, since, scheduled, sub) + return s.sendOldMessages(topics, since, scheduled, v, sub) } subscriberIDs := make([]int, 0) for _, t := range topics { @@ -873,10 +871,10 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi topics[i].Unsubscribe(subscriberID) // Order! } }() - if err := sub(newOpenMessage(topicsStr)); err != nil { // Send out open message + if err := sub(v, newOpenMessage(topicsStr)); err != nil { // Send out open message return err } - if err := s.sendOldMessages(topics, since, scheduled, sub); err != nil { + if err := s.sendOldMessages(topics, since, scheduled, v, sub); err != nil { return err } err = g.Wait() @@ -900,7 +898,7 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu return } -func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, sub subscriber) error { +func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, v *visitor, sub subscriber) error { if since.IsNone() { return nil } @@ -910,7 +908,7 @@ func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled b return err } for _, m := range messages { - if err := sub(m); err != nil { + if err := sub(v, m); err != nil { return err } } @@ -1057,23 +1055,7 @@ func (s *Server) updateStatsAndPrune() { } func (s *Server) runSMTPServer() error { - sub := func(m *message) error { - url := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic) - req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message)) - if err != nil { - return err - } - if m.Title != "" { - req.Header.Set("Title", m.Title) - } - rr := httptest.NewRecorder() - s.handle(rr, req) - if rr.Code != http.StatusOK { - return errors.New("error: " + rr.Body.String()) - } - return nil - } - s.smtpBackend = newMailBackend(s.config, sub) + s.smtpBackend = newMailBackend(s.config, s.handle) s.smtpServer = smtp.NewServer(s.smtpBackend) s.smtpServer.Addr = s.config.SMTPServerListen s.smtpServer.Domain = s.config.SMTPServerDomain @@ -1096,7 +1078,7 @@ func (s *Server) runManager() { } } -func (s *Server) runAtSender() { +func (s *Server) runDelayedSender() { for { select { case <-time.After(s.config.AtSenderInterval): @@ -1113,14 +1095,15 @@ func (s *Server) runFirebaseKeepaliver() { if s.firebase == nil { return } + v := newVisitor(s.config, s.messageCache, "0.0.0.0") for { select { case <-time.After(s.config.FirebaseKeepaliveInterval): - if err := s.firebase(newKeepaliveMessage(firebaseControlTopic)); err != nil { + if err := s.firebase(v, newKeepaliveMessage(firebaseControlTopic)); err != nil { 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 { + if err := s.firebase(v, newKeepaliveMessage(firebasePollTopic)); err != nil { log.Printf("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error()) } case <-s.closeChan: @@ -1130,28 +1113,36 @@ func (s *Server) runFirebaseKeepaliver() { } func (s *Server) sendDelayedMessages() error { - s.mu.Lock() - defer s.mu.Unlock() messages, err := s.messageCache.MessagesDue() if err != nil { return err } for _, m := range messages { - t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published - if ok { - if err := t.Publish(m); err != nil { - log.Printf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) - } + v := s.visitorFromIP("0.0.0.0") // FIXME: get message owner!! + if err := s.sendDelayedMessage(v, m); err != nil { + log.Printf("error sending delayed message: %s", err.Error()) } - if s.firebase != nil { // Firebase subscribers may not show up in topics map - if err := s.firebase(m); err != nil { - log.Printf("unable to publish to Firebase: %v", err.Error()) - } + } + return nil +} + +func (s *Server) sendDelayedMessage(v *visitor, m *message) error { + s.mu.Lock() + defer s.mu.Unlock() + t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published + if ok { + if err := t.Publish(v, m); err != nil { + return fmt.Errorf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) } - if err := s.messageCache.MarkPublished(m); err != nil { - return err + } + if s.firebase != nil { // Firebase subscribers may not show up in topics map + if err := s.firebase(v, m); err != nil { + return fmt.Errorf("unable to publish to Firebase: %v", err.Error()) } } + if err := s.messageCache.MarkPublished(m); err != nil { + return err + } return nil } @@ -1290,8 +1281,6 @@ func extractUserPass(r *http.Request) (username string, password string, ok bool // visitor creates or retrieves a rate.Limiter for the given visitor. // This function was taken from https://www.alexedwards.net/blog/how-to-rate-limit-http-requests (MIT). func (s *Server) visitor(r *http.Request) *visitor { - s.mu.Lock() - defer s.mu.Unlock() remoteAddr := r.RemoteAddr ip, _, err := net.SplitHostPort(remoteAddr) if err != nil { @@ -1300,6 +1289,12 @@ func (s *Server) visitor(r *http.Request) *visitor { if s.config.BehindProxy && r.Header.Get("X-Forwarded-For") != "" { ip = r.Header.Get("X-Forwarded-For") } + return s.visitorFromIP(ip) +} + +func (s *Server) visitorFromIP(ip string) *visitor { + s.mu.Lock() + defer s.mu.Unlock() v, exists := s.visitors[ip] if !exists { s.visitors[ip] = newVisitor(s.config, s.messageCache, ip) diff --git a/server/server_firebase.go b/server/server_firebase.go index 1facd5da..83683370 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "strings" firebase "firebase.google.com/go/v4" @@ -26,12 +27,20 @@ func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subsc if err != nil { return nil, err } - return func(m *message) error { + return func(v *visitor, m *message) error { + if err := v.FirebaseAllowed(); err != nil { + return errHTTPTooManyRequestsFirebaseQuotaReached + } fbm, err := toFirebaseMessage(m, auther) if err != nil { return err } _, err = msg.Send(context.Background(), fbm) + if err != nil && messaging.IsQuotaExceeded(err) { + log.Printf("[%s] FB quota exceeded when trying to publish to topic %s, temporarily denying FB access", v.ip, m.Topic) + v.FirebaseTemporarilyDeny() + return errHTTPTooManyRequestsFirebaseQuotaReached + } return err }, nil } diff --git a/server/server_test.go b/server/server_test.go index 06f3cd2d..5e23e47e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -469,7 +469,8 @@ func TestServer_PublishFirebase(t *testing.T) { require.NotEmpty(t, msg.ID) // Keepalive message - require.Nil(t, s.firebase(newKeepaliveMessage(firebaseControlTopic))) + v := newVisitor(s.config, s.messageCache, "1.2.3.4") + require.Nil(t, s.firebase(v, newKeepaliveMessage(firebaseControlTopic))) time.Sleep(500 * time.Millisecond) // Time for sends } diff --git a/server/smtp_server.go b/server/smtp_server.go index c437d235..a5b6f850 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -3,10 +3,13 @@ package server import ( "bytes" "errors" + "fmt" "github.com/emersion/go-smtp" "io" "mime" "mime/multipart" + "net/http" + "net/http/httptest" "net/mail" "strings" "sync" @@ -23,25 +26,25 @@ var ( // smtpBackend implements SMTP server methods. type smtpBackend struct { config *Config - sub subscriber + handler func(http.ResponseWriter, *http.Request) success int64 failure int64 mu sync.Mutex } -func newMailBackend(conf *Config, sub subscriber) *smtpBackend { +func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Request)) *smtpBackend { return &smtpBackend{ - config: conf, - sub: sub, + config: conf, + handler: handler, } } func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { - return &smtpSession{backend: b}, nil + return &smtpSession{backend: b, remoteAddr: state.RemoteAddr.String()}, nil } func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { - return &smtpSession{backend: b}, nil + return &smtpSession{backend: b, remoteAddr: state.RemoteAddr.String()}, nil } func (b *smtpBackend) Counts() (success int64, failure int64) { @@ -52,9 +55,10 @@ func (b *smtpBackend) Counts() (success int64, failure int64) { // smtpSession is returned after EHLO. type smtpSession struct { - backend *smtpBackend - topic string - mu sync.Mutex + backend *smtpBackend + remoteAddr string + topic string + mu sync.Mutex } func (s *smtpSession) AuthPlain(username, password string) error { @@ -128,7 +132,7 @@ func (s *smtpSession) Data(r io.Reader) error { m.Message = m.Title // Flip them, this makes more sense m.Title = "" } - if err := s.backend.sub(m); err != nil { + if err := s.publishMessage(m); err != nil { return err } s.backend.mu.Lock() @@ -138,6 +142,24 @@ func (s *smtpSession) Data(r io.Reader) error { }) } +func (s *smtpSession) publishMessage(m *message) error { + url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) + req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message)) + req.RemoteAddr = s.remoteAddr // rate limiting!! + if err != nil { + return err + } + if m.Title != "" { + req.Header.Set("Title", m.Title) + } + rr := httptest.NewRecorder() + s.backend.handler(rr, req) + if rr.Code != http.StatusOK { + return errors.New("error: " + rr.Body.String()) + } + return nil +} + func (s *smtpSession) Reset() { s.mu.Lock() s.topic = "" diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index d0e8bfd2..8e9d5892 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -3,6 +3,9 @@ package server import ( "github.com/emersion/go-smtp" "github.com/stretchr/testify/require" + "io" + "net" + "net/http" "strings" "testing" ) @@ -27,13 +30,12 @@ Content-Type: text/html; charset="UTF-8"
what's up

--000000000000f3320b05d42915c9--` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "and one more", m.Title) - require.Equal(t, "what's up", m.Message) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "and one more", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -59,13 +61,12 @@ Content-Type: text/html; charset="UTF-8"

--000000000000bcf4a405d429f8d4--` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "emailtest", m.Topic) - require.Equal(t, "", m.Title) // We flipped message and body - require.Equal(t, "This email has a subject but no body", m.Message) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/emailtest", r.URL.Path) + require.Equal(t, "", r.Header.Get("Title")) // We flipped message and body + require.Equal(t, "This email has a subject but no body", readAll(t, r.Body)) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-emailtest@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -81,14 +82,13 @@ Content-Type: text/plain; charset="UTF-8" what's up ` - conf, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "and one more", m.Title) - require.Equal(t, "what's up", m.Message) - return nil + conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "and one more", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -99,14 +99,13 @@ func TestSmtpBackend_Plaintext_No_ContentType(t *testing.T) { 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, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "Very short mail", r.Header.Get("Title")) + require.Equal(t, "what's up", readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -121,11 +120,10 @@ Content-Type: text/plain; charset="UTF-8" what's up ` - _, backend := newTestBackend(t, func(m *message) error { - require.Equal(t, "Three santas 🎅🎅🎅", m.Title) - return nil + _, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "Three santas 🎅🎅🎅", r.Header.Get("Title")) }) - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("ntfy-mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -140,7 +138,7 @@ To: mytopic@ntfy.sh Content-Type: text/plain; charset="UTF-8" you know this is a string. -it's a long string. +it's a long string. it's supposed to be longer than the max message length which is 4096 bytes, it used to be 512 bytes, but I increased that for the UnifiedPush support @@ -204,9 +202,9 @@ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB that should do it ` - conf, backend := newTestBackend(t, func(m *message) error { + conf, backend := newTestBackend(t, func(w http.ResponseWriter, r *http.Request) { expected := `you know this is a string. -it's a long string. +it's a long string. it's supposed to be longer than the max message length which is 4096 bytes, it used to be 512 bytes, but I increased that for the UnifiedPush support @@ -266,13 +264,12 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ...................................................................... ...................................................................... and with BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB -BBBBBBBBBBBBBBBBBBBBBBBB` +BBBBBBBBBBBBBBBBBBBBBBBBB` require.Equal(t, 4096, len(expected)) // Sanity check - require.Equal(t, expected, m.Message) - return nil + require.Equal(t, expected, readAll(t, r.Body)) }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.AnonymousLogin(nil) + session, _ := backend.AnonymousLogin(fakeConnState(t, "1.2.3.4")) require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Nil(t, session.Data(strings.NewReader(email))) @@ -288,21 +285,41 @@ Content-Type: text/SOMETHINGELSE what's up ` - conf, backend := newTestBackend(t, func(m *message) error { - return nil + conf, backend := newTestBackend(t, func(http.ResponseWriter, *http.Request) { + // Nothing. }) conf.SMTPServerAddrPrefix = "" - session, _ := backend.Login(nil, "user", "pass") + session, _ := backend.Login(fakeConnState(t, "1.2.3.4"), "user", "pass") require.Nil(t, session.Mail("phil@example.com", smtp.MailOptions{})) require.Nil(t, session.Rcpt("mytopic@ntfy.sh")) require.Equal(t, errUnsupportedContentType, session.Data(strings.NewReader(email))) } -func newTestBackend(t *testing.T, sub subscriber) (*Config, *smtpBackend) { +func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Request)) (*Config, *smtpBackend) { conf := newTestConfig(t) conf.SMTPServerListen = ":25" conf.SMTPServerDomain = "ntfy.sh" conf.SMTPServerAddrPrefix = "ntfy-" - backend := newMailBackend(conf, sub) + backend := newMailBackend(conf, handler) return conf, backend } + +func readAll(t *testing.T, rc io.ReadCloser) string { + b, err := io.ReadAll(rc) + if err != nil { + t.Fatal(err) + } + return string(b) +} + +func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState { + ip, err := net.ResolveIPAddr("ip", remoteAddr) + if err != nil { + t.Fatal(err) + } + return &smtp.ConnectionState{ + Hostname: "myhostname", + LocalAddr: ip, + RemoteAddr: ip, + } +} diff --git a/server/topic.go b/server/topic.go index 9badd7bd..eb53225b 100644 --- a/server/topic.go +++ b/server/topic.go @@ -15,7 +15,7 @@ type topic struct { } // subscriber is a function that is called for every new message on a topic -type subscriber func(msg *message) error +type subscriber func(v *visitor, msg *message) error // newTopic creates a new topic func newTopic(id string) *topic { @@ -42,12 +42,12 @@ func (t *topic) Unsubscribe(id int) { } // Publish asynchronously publishes to all subscribers -func (t *topic) Publish(m *message) error { +func (t *topic) Publish(v *visitor, m *message) error { go func() { t.mu.Lock() defer t.mu.Unlock() for _, s := range t.subscribers { - if err := s(m); err != nil { + if err := s(v, m); err != nil { log.Printf("error publishing message to subscriber") } } diff --git a/server/visitor.go b/server/visitor.go index 58cc28ab..1bbc4e00 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -28,6 +28,7 @@ type visitor struct { emails *rate.Limiter subscriptions util.Limiter bandwidth util.Limiter + firebase time.Time // Next allowed Firebase message seen time.Time mu sync.Mutex } @@ -48,14 +49,11 @@ func newVisitor(conf *Config, messageCache *messageCache, ip string) *visitor { emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), bandwidth: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), + firebase: time.Unix(0, 0), seen: time.Now(), } } -func (v *visitor) IP() string { - return v.ip -} - func (v *visitor) RequestAllowed() error { if !v.requests.Allow() { return errVisitorLimitReached @@ -63,6 +61,21 @@ func (v *visitor) RequestAllowed() error { return nil } +func (v *visitor) FirebaseAllowed() error { + v.mu.Lock() + defer v.mu.Unlock() + if time.Now().Before(v.firebase) { + return errVisitorLimitReached + } + return nil +} + +func (v *visitor) FirebaseTemporarilyDeny() { + v.mu.Lock() + defer v.mu.Unlock() + v.firebase = time.Now().Add(v.config.FirebaseQuotaLimitPenaltyDuration) +} + func (v *visitor) EmailAllowed() error { if !v.emails.Allow() { return errVisitorLimitReached From f9284a098a7b47906918e0a23bf38b08a666b537 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 21:39:19 -0400 Subject: [PATCH 10/38] Store Sender IP in DB for delayed messages --- server/config.go | 4 +-- server/message_cache.go | 51 ++++++++++++++++++++++------------ server/message_cache_test.go | 10 +++---- server/server.go | 30 ++++++++++++-------- server/server_firebase_test.go | 1 - server/server_test.go | 17 ++++++++---- server/types.go | 2 +- 7 files changed, 73 insertions(+), 42 deletions(-) diff --git a/server/config.go b/server/config.go index 3de4bd70..7f15aedc 100644 --- a/server/config.go +++ b/server/config.go @@ -67,7 +67,7 @@ type Config struct { KeepaliveInterval time.Duration ManagerInterval time.Duration WebRootIsApp bool - AtSenderInterval time.Duration + DelayedSenderInterval time.Duration FirebaseKeepaliveInterval time.Duration FirebasePollInterval time.Duration FirebaseQuotaLimitPenaltyDuration time.Duration @@ -120,7 +120,7 @@ func NewConfig() *Config { MessageLimit: DefaultMessageLengthLimit, MinDelay: DefaultMinDelay, MaxDelay: DefaultMaxDelay, - AtSenderInterval: DefaultAtSenderInterval, + DelayedSenderInterval: DefaultAtSenderInterval, FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, FirebasePollInterval: DefaultFirebasePollInterval, FirebaseQuotaLimitPenaltyDuration: DefaultFirebaseQuotaLimitPenaltyDuration, diff --git a/server/message_cache.go b/server/message_cache.go index b55c34ba..4dc83bdf 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -36,7 +36,7 @@ const ( attachment_size INT NOT NULL, attachment_expires INT NOT NULL, attachment_url TEXT NOT NULL, - attachment_owner TEXT NOT NULL, + sender TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -45,37 +45,37 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published) + INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?` selectMessagesSinceTimeQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesDueQuery = ` - SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding + SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -84,13 +84,13 @@ const ( selectMessagesCountQuery = `SELECT COUNT(*) FROM messages` selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?` selectTopicsQuery = `SELECT topic FROM messages GROUP BY topic` - selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE attachment_owner = ? AND attachment_expires >= ?` + selectAttachmentsSizeQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE sender = ? AND attachment_expires >= ?` selectAttachmentsExpiredQuery = `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires < ?` ) // Schema management queries const ( - currentSchemaVersion = 6 + currentSchemaVersion = 7 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -173,6 +173,11 @@ const ( migrate5To6AlterMessagesTableQuery = ` ALTER TABLE messages ADD COLUMN actions TEXT NOT NULL DEFAULT(''); ` + + // 6 -> 7 + migrate6To7AlterMessagesTableQuery = ` + ALTER TABLE messages RENAME COLUMN attachment_owner TO sender; + ` ) type messageCache struct { @@ -225,7 +230,7 @@ func (c *messageCache) AddMessage(m *message) error { } published := m.Time <= time.Now().Unix() tags := strings.Join(m.Tags, ",") - var attachmentName, attachmentType, attachmentURL, attachmentOwner string + var attachmentName, attachmentType, attachmentURL string var attachmentSize, attachmentExpires int64 if m.Attachment != nil { attachmentName = m.Attachment.Name @@ -233,7 +238,6 @@ func (c *messageCache) AddMessage(m *message) error { attachmentSize = m.Attachment.Size attachmentExpires = m.Attachment.Expires attachmentURL = m.Attachment.URL - attachmentOwner = m.Attachment.Owner } var actionsStr string if len(m.Actions) > 0 { @@ -259,7 +263,7 @@ func (c *messageCache) AddMessage(m *message) error { attachmentSize, attachmentExpires, attachmentURL, - attachmentOwner, + m.Sender, m.Encoding, published, ) @@ -371,8 +375,8 @@ func (c *messageCache) Prune(olderThan time.Time) error { return err } -func (c *messageCache) AttachmentBytesUsed(owner string) (int64, error) { - rows, err := c.db.Query(selectAttachmentsSizeQuery, owner, time.Now().Unix()) +func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { + rows, err := c.db.Query(selectAttachmentsSizeQuery, sender, time.Now().Unix()) if err != nil { return 0, err } @@ -415,7 +419,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { for rows.Next() { var timestamp, attachmentSize, attachmentExpires int64 var priority int - var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string + var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, sender, encoding string err := rows.Scan( &id, ×tamp, @@ -431,7 +435,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { &attachmentSize, &attachmentExpires, &attachmentURL, - &attachmentOwner, + &sender, &encoding, ) if err != nil { @@ -455,7 +459,6 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Size: attachmentSize, Expires: attachmentExpires, URL: attachmentURL, - Owner: attachmentOwner, } } messages = append(messages, &message{ @@ -470,6 +473,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { Click: click, Actions: actions, Attachment: att, + Sender: sender, Encoding: encoding, }) } @@ -516,6 +520,8 @@ func setupCacheDB(db *sql.DB) error { return migrateFrom4(db) } else if schemaVersion == 5 { return migrateFrom5(db) + } else if schemaVersion == 6 { + return migrateFrom6(db) } return fmt.Errorf("unexpected schema version found: %d", schemaVersion) } @@ -599,5 +605,16 @@ func migrateFrom5(db *sql.DB) error { if _, err := db.Exec(updateSchemaVersion, 6); err != nil { return err } + return migrateFrom6(db) +} + +func migrateFrom6(db *sql.DB) error { + log.Print("Migrating cache database schema: from 6 to 7") + if _, err := db.Exec(migrate6To7AlterMessagesTableQuery); err != nil { + return err + } + if _, err := db.Exec(updateSchemaVersion, 7); err != nil { + return err + } return nil // Update this when a new version is added } diff --git a/server/message_cache_test.go b/server/message_cache_test.go index cb888b42..398f21e4 100644 --- a/server/message_cache_test.go +++ b/server/message_cache_test.go @@ -281,39 +281,39 @@ func testCacheAttachments(t *testing.T, c *messageCache) { expires1 := time.Now().Add(-4 * time.Hour).Unix() m := newDefaultMessage("mytopic", "flower for you") m.ID = "m1" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "flower.jpg", Type: "image/jpeg", Size: 5000, Expires: expires1, URL: "https://ntfy.sh/file/AbDeFgJhal.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) expires2 := time.Now().Add(2 * time.Hour).Unix() // Future m = newDefaultMessage("mytopic", "sending you a car") m.ID = "m2" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "car.jpg", Type: "image/jpeg", Size: 10000, Expires: expires2, URL: "https://ntfy.sh/file/aCaRURL.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) expires3 := time.Now().Add(1 * time.Hour).Unix() // Future m = newDefaultMessage("another-topic", "sending you another car") m.ID = "m3" + m.Sender = "1.2.3.4" m.Attachment = &attachment{ Name: "another-car.jpg", Type: "image/jpeg", Size: 20000, Expires: expires3, URL: "https://ntfy.sh/file/zakaDHFW.jpg", - Owner: "1.2.3.4", } require.Nil(t, c.AddMessage(m)) @@ -327,7 +327,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { require.Equal(t, int64(5000), messages[0].Attachment.Size) require.Equal(t, expires1, messages[0].Attachment.Expires) require.Equal(t, "https://ntfy.sh/file/AbDeFgJhal.jpg", messages[0].Attachment.URL) - require.Equal(t, "1.2.3.4", messages[0].Attachment.Owner) + require.Equal(t, "1.2.3.4", messages[0].Sender) require.Equal(t, "sending you a car", messages[1].Message) require.Equal(t, "car.jpg", messages[1].Attachment.Name) @@ -335,7 +335,7 @@ func testCacheAttachments(t *testing.T, c *messageCache) { require.Equal(t, int64(10000), messages[1].Attachment.Size) require.Equal(t, expires2, messages[1].Attachment.Expires) require.Equal(t, "https://ntfy.sh/file/aCaRURL.jpg", messages[1].Attachment.URL) - require.Equal(t, "1.2.3.4", messages[1].Attachment.Owner) + require.Equal(t, "1.2.3.4", messages[1].Sender) size, err := c.AttachmentBytesUsed("1.2.3.4") require.Nil(t, err) diff --git a/server/server.go b/server/server.go index 2baa3666..7384ab47 100644 --- a/server/server.go +++ b/server/server.go @@ -443,7 +443,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito if s.mailer != nil && email != "" && !delayed { go s.sendEmail(v, m, email) } - if s.config.UpstreamBaseURL != "" { + if s.config.UpstreamBaseURL != "" && !delayed { go s.forwardPollRequest(v, m) } if cache { @@ -484,7 +484,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { return } req.Header.Set("X-Poll-ID", m.ID) - response, err := http.DefaultClient.Do(req) + var httpClient = &http.Client{ + Timeout: time.Second * 10, + } + response, err := httpClient.Do(req) if err != nil { log.Printf("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) return @@ -566,6 +569,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca return false, false, "", false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() + m.Sender = v.ip // Important for rate limiting } actionsStr := readParam(r, "x-actions", "actions", "action") if actionsStr != "" { @@ -661,7 +665,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, m.Attachment = &attachment{} } var ext string - m.Attachment.Owner = v.ip // Important for attachment rate limiting + m.Sender = v.ip // Important for attachment rate limiting m.Attachment.Expires = time.Now().Add(s.config.AttachmentExpiryDuration).Unix() m.Attachment.Type, ext = util.DetectContentType(body.PeekedBytes, m.Attachment.Name) m.Attachment.URL = fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext) @@ -1081,7 +1085,7 @@ func (s *Server) runManager() { func (s *Server) runDelayedSender() { for { select { - case <-time.After(s.config.AtSenderInterval): + case <-time.After(s.config.DelayedSenderInterval): if err := s.sendDelayedMessages(); err != nil { log.Printf("error sending scheduled messages: %s", err.Error()) } @@ -1118,7 +1122,7 @@ func (s *Server) sendDelayedMessages() error { return err } for _, m := range messages { - v := s.visitorFromIP("0.0.0.0") // FIXME: get message owner!! + v := s.visitorFromIP(m.Sender) if err := s.sendDelayedMessage(v, m); err != nil { log.Printf("error sending delayed message: %s", err.Error()) } @@ -1131,14 +1135,18 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error { defer s.mu.Unlock() t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published if ok { - if err := t.Publish(v, m); err != nil { - return fmt.Errorf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) - } + go func() { + // We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler + if err := t.Publish(v, m); err != nil { + log.Printf("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) + } + }() } if s.firebase != nil { // Firebase subscribers may not show up in topics map - if err := s.firebase(v, m); err != nil { - return fmt.Errorf("unable to publish to Firebase: %v", err.Error()) - } + go s.sendToFirebase(v, m) + } + if s.config.UpstreamBaseURL != "" { + go s.forwardPollRequest(v, m) } if err := s.messageCache.MarkPublished(m); err != nil { return err diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index f3904fac..6ad6fde9 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -119,7 +119,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { Size: 12345, Expires: 98765543, URL: "https://example.com/file.jpg", - Owner: "some-owner", } fbm, err := toFirebaseMessage(m, &testAuther{Allow: true}) require.Nil(t, err) diff --git a/server/server_test.go b/server/server_test.go index 5e23e47e..1fec1f56 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -264,7 +264,7 @@ func TestServer_PublishNoCache(t *testing.T) { func TestServer_PublishAt(t *testing.T) { c := newTestConfig(t) c.MinDelay = time.Second - c.AtSenderInterval = 100 * time.Millisecond + c.DelayedSenderInterval = 100 * time.Millisecond s := newTestServer(t, c) response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ @@ -283,6 +283,13 @@ func TestServer_PublishAt(t *testing.T) { messages = toMessages(t, response.Body.String()) require.Equal(t, 1, len(messages)) require.Equal(t, "a message", messages[0].Message) + require.Equal(t, "", messages[0].Sender) // Never return the sender! + + messages, err := s.messageCache.Messages("mytopic", sinceAllMessages, true) + require.Nil(t, err) + require.Equal(t, 1, len(messages)) + require.Equal(t, "a message", messages[0].Message) + require.Equal(t, "9.9.9.9", messages[0].Sender) // It's stored in the DB though! } func TestServer_PublishAtWithCacheError(t *testing.T) { @@ -1019,7 +1026,7 @@ func TestServer_PublishAttachment(t *testing.T) { require.Equal(t, int64(5000), msg.Attachment.Size) require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, "", msg.Attachment.Owner) // Should never be returned + require.Equal(t, "", msg.Sender) // Should never be returned require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") @@ -1048,7 +1055,7 @@ func TestServer_PublishAttachmentShortWithFilename(t *testing.T) { require.Equal(t, int64(21), msg.Attachment.Size) require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix()) require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, "", msg.Attachment.Owner) // Should never be returned + require.Equal(t, "", msg.Sender) // Should never be returned require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") @@ -1075,7 +1082,7 @@ func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) { require.Equal(t, "", msg.Attachment.Type) require.Equal(t, int64(0), msg.Attachment.Size) require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, "", msg.Attachment.Owner) + require.Equal(t, "", msg.Sender) // Slightly unrelated cross-test: make sure we don't add an owner for external attachments size, err := s.messageCache.AttachmentBytesUsed("127.0.0.1") @@ -1096,7 +1103,7 @@ func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) { require.Equal(t, "", msg.Attachment.Type) require.Equal(t, int64(0), msg.Attachment.Size) require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, "", msg.Attachment.Owner) + require.Equal(t, "", msg.Sender) } func TestServer_PublishAttachmentBadURL(t *testing.T) { diff --git a/server/types.go b/server/types.go index 6a69338c..bb8e32a3 100644 --- a/server/types.go +++ b/server/types.go @@ -32,6 +32,7 @@ type message struct { Actions []*action `json:"actions,omitempty"` Attachment *attachment `json:"attachment,omitempty"` PollID string `json:"poll_id,omitempty"` + Sender string `json:"-"` // IP address of uploader, used for rate limiting Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes } @@ -41,7 +42,6 @@ type attachment struct { Size int64 `json:"size,omitempty"` Expires int64 `json:"expires,omitempty"` URL string `json:"url"` - Owner string `json:"-"` // IP address of uploader, used for rate limiting } type action struct { From c80e4e1aa9318b85a8abb67fe623451418079fe6 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 23:16:44 -0400 Subject: [PATCH 11/38] Make Firebase logic testable, test it --- server/server.go | 72 +++++++++++++++---------------- server/server_firebase.go | 77 ++++++++++++++++++++++++++-------- server/server_firebase_test.go | 38 +++++++++++++++++ server/server_test.go | 49 +++++++--------------- 4 files changed, 147 insertions(+), 89 deletions(-) diff --git a/server/server.go b/server/server.go index 7384ab47..253a422b 100644 --- a/server/server.go +++ b/server/server.go @@ -32,22 +32,22 @@ import ( // Server is the main server, providing the UI and API for ntfy type Server struct { - config *Config - httpServer *http.Server - httpsServer *http.Server - unixListener net.Listener - smtpServer *smtp.Server - smtpBackend *smtpBackend - topics map[string]*topic - visitors map[string]*visitor - firebase subscriber - mailer mailer - messages int64 - auth auth.Auther - messageCache *messageCache - fileCache *fileCache - closeChan chan bool - mu sync.Mutex + config *Config + httpServer *http.Server + httpsServer *http.Server + unixListener net.Listener + smtpServer *smtp.Server + smtpBackend *smtpBackend + topics map[string]*topic + visitors map[string]*visitor + firebaseClient *firebaseClient + mailer mailer + messages int64 + auth auth.Auther + messageCache *messageCache + fileCache *fileCache + closeChan chan bool + mu sync.Mutex } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -134,23 +134,23 @@ func New(conf *Config) (*Server, error) { return nil, err } } - var firebaseSubscriber subscriber + var firebaseClient *firebaseClient if conf.FirebaseKeyFile != "" { - var err error - firebaseSubscriber, err = createFirebaseSubscriber(conf.FirebaseKeyFile, auther) + sender, err := newFirebaseSender(conf.FirebaseKeyFile) if err != nil { return nil, err } + firebaseClient = newFirebaseClient(sender, auther) } return &Server{ - config: conf, - messageCache: messageCache, - fileCache: fileCache, - firebase: firebaseSubscriber, - mailer: mailer, - topics: topics, - auth: auther, - visitors: make(map[string]*visitor), + config: conf, + messageCache: messageCache, + fileCache: fileCache, + firebaseClient: firebaseClient, + mailer: mailer, + topics: topics, + auth: auther, + visitors: make(map[string]*visitor), }, nil } @@ -437,7 +437,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } } - if s.firebase != nil && firebase && !delayed { + if s.firebaseClient != nil && firebase && !delayed { go s.sendToFirebase(v, m) } if s.mailer != nil && email != "" && !delayed { @@ -463,7 +463,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) sendToFirebase(v *visitor, m *message) { - if err := s.firebase(v, m); err != nil { + if err := s.firebaseClient.Send(v, m); err != nil { log.Printf("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error()) } } @@ -1096,20 +1096,16 @@ func (s *Server) runDelayedSender() { } func (s *Server) runFirebaseKeepaliver() { - if s.firebase == nil { + if s.firebaseClient == nil { return } - v := newVisitor(s.config, s.messageCache, "0.0.0.0") + v := newVisitor(s.config, s.messageCache, "0.0.0.0") // Background process, not a real visitor for { select { case <-time.After(s.config.FirebaseKeepaliveInterval): - if err := s.firebase(v, newKeepaliveMessage(firebaseControlTopic)); err != nil { - log.Printf("error sending Firebase keepalive message to %s: %s", firebaseControlTopic, err.Error()) - } + s.sendToFirebase(v, newKeepaliveMessage(firebaseControlTopic)) case <-time.After(s.config.FirebasePollInterval): - if err := s.firebase(v, newKeepaliveMessage(firebasePollTopic)); err != nil { - log.Printf("error sending Firebase keepalive message to %s: %s", firebasePollTopic, err.Error()) - } + s.sendToFirebase(v, newKeepaliveMessage(firebasePollTopic)) case <-s.closeChan: return } @@ -1142,7 +1138,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error { } }() } - if s.firebase != nil { // Firebase subscribers may not show up in topics map + if s.firebaseClient != nil { // Firebase subscribers may not show up in topics map go s.sendToFirebase(v, m) } if s.config.UpstreamBaseURL != "" { diff --git a/server/server_firebase.go b/server/server_firebase.go index 83683370..47d27555 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -3,6 +3,7 @@ package server import ( "context" "encoding/json" + "errors" "fmt" "log" "strings" @@ -18,33 +19,75 @@ const ( fcmApnsBodyMessageLimit = 100 ) -func createFirebaseSubscriber(credentialsFile string, auther auth.Auther) (subscriber, error) { +var ( + errFirebaseQuotaExceeded = errors.New("Firebase quota exceeded") +) + +// firebaseClient is a generic client that formats and sends messages to Firebase. +// The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable. +type firebaseClient struct { + sender firebaseSender + auther auth.Auther +} + +func newFirebaseClient(sender firebaseSender, auther auth.Auther) *firebaseClient { + return &firebaseClient{ + sender: sender, + auther: auther, + } +} + +func (c *firebaseClient) Send(v *visitor, m *message) error { + if err := v.FirebaseAllowed(); err != nil { + return errFirebaseQuotaExceeded + } + fbm, err := toFirebaseMessage(m, c.auther) + if err != nil { + return err + } + err = c.sender.Send(fbm) + if err == errFirebaseQuotaExceeded { + log.Printf("[%s] FB quota exceeded for topic %s, temporarily denying FB access to visitor", v.ip, m.Topic) + v.FirebaseTemporarilyDeny() + } + return err +} + +// firebaseSender is an interface that represents a client that can send to Firebase Cloud Messaging. +// In tests, this can be implemented with a mock. +type firebaseSender interface { + // Send sends a message to Firebase, or returns an error. It returns errFirebaseQuotaExceeded + // if a rate limit has reached. + Send(m *messaging.Message) error +} + +// firebaseSenderImpl is a firebaseSender that actually talks to Firebase +type firebaseSenderImpl struct { + client *messaging.Client +} + +func newFirebaseSender(credentialsFile string) (*firebaseSenderImpl, error) { fb, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsFile(credentialsFile)) if err != nil { return nil, err } - msg, err := fb.Messaging(context.Background()) + client, err := fb.Messaging(context.Background()) if err != nil { return nil, err } - return func(v *visitor, m *message) error { - if err := v.FirebaseAllowed(); err != nil { - return errHTTPTooManyRequestsFirebaseQuotaReached - } - fbm, err := toFirebaseMessage(m, auther) - if err != nil { - return err - } - _, err = msg.Send(context.Background(), fbm) - if err != nil && messaging.IsQuotaExceeded(err) { - log.Printf("[%s] FB quota exceeded when trying to publish to topic %s, temporarily denying FB access", v.ip, m.Topic) - v.FirebaseTemporarilyDeny() - return errHTTPTooManyRequestsFirebaseQuotaReached - } - return err + return &firebaseSenderImpl{ + client: client, }, nil } +func (c *firebaseSenderImpl) Send(m *messaging.Message) error { + _, err := c.client.Send(context.Background(), m) + if err != nil && messaging.IsQuotaExceeded(err) { + return errFirebaseQuotaExceeded + } + return err +} + // toFirebaseMessage converts a message to a Firebase message. // // Normal messages ("message"): diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 6ad6fde9..8e08b0d6 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -26,6 +26,25 @@ func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error { return errors.New("unauthorized") } +type testFirebaseSender struct { + allowed int + messages []*messaging.Message +} + +func newTestFirebaseSender(allowed int) *testFirebaseSender { + return &testFirebaseSender{ + allowed: allowed, + messages: make([]*messaging.Message, 0), + } +} +func (s *testFirebaseSender) Send(m *messaging.Message) error { + if len(s.messages)+1 > s.allowed { + return errFirebaseQuotaExceeded + } + s.messages = append(s.messages, m) + return nil +} + func TestToFirebaseMessage_Keepalive(t *testing.T) { m := newKeepaliveMessage("mytopic") fbm, err := toFirebaseMessage(m, nil) @@ -285,3 +304,22 @@ func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) { require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage)) require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"]) } + +func TestToFirebaseSender_Abuse(t *testing.T) { + sender := &testFirebaseSender{allowed: 2} + client := newFirebaseClient(sender, &testAuther{}) + visitor := newVisitor(newTestConfig(t), newMemTestCache(t), "1.2.3.4") + + require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 1, len(sender.messages)) + + require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 2, len(sender.messages)) + + require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 2, len(sender.messages)) + + sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working + require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, 0, len(sender.messages)) +} diff --git a/server/server_test.go b/server/server_test.go index 1fec1f56..d05075fd 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -9,7 +9,6 @@ import ( "math/rand" "net/http" "net/http/httptest" - "os" "path/filepath" "strings" "sync" @@ -55,6 +54,21 @@ func TestServer_PublishAndPoll(t *testing.T) { require.Equal(t, "my second message", lines[1]) // \n -> " " } +func TestServer_PublishWithFirebase(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + + response := request(t, s, "PUT", "/mytopic", "my first message", nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + require.Equal(t, 1, len(sender.messages)) + require.Equal(t, "my first message", sender.messages[0].Data["message"]) + require.Equal(t, "my first message", sender.messages[0].APNS.Payload.Aps.Alert.Body) + require.Equal(t, "my first message", sender.messages[0].APNS.Payload.CustomData["message"]) +} + func TestServer_SubscribeOpenAndKeepalive(t *testing.T) { c := newTestConfig(t) c.KeepaliveInterval = time.Second @@ -461,27 +475,6 @@ func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) { require.Equal(t, "Line 1\nLine 2", msg.Message) // \\n -> \n ! } -func TestServer_PublishFirebase(t *testing.T) { - // This is unfortunately not much of a test, since it merely fires the messages towards Firebase, - // but cannot re-read them. There is no way from Go to read the messages back, or even get an error back. - // I tried everything. I already had written the test, and it increases the code coverage, so I'll leave it ... :shrug: ... - - c := newTestConfig(t) - c.FirebaseKeyFile = firebaseServiceAccountFile(t) // May skip the test! - s := newTestServer(t, c) - - // Normal message - response := request(t, s, "PUT", "/mytopic", "This is a message for firebase", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - - // Keepalive message - v := newVisitor(s.config, s.messageCache, "1.2.3.4") - require.Nil(t, s.firebase(v, newKeepaliveMessage(firebaseControlTopic))) - - time.Sleep(500 * time.Millisecond) // Time for sends -} - func TestServer_PublishInvalidTopic(t *testing.T) { s := newTestServer(t, newTestConfig(t)) s.mailer = &testMailer{} @@ -1341,18 +1334,6 @@ func toHTTPError(t *testing.T, s string) *errHTTP { return &e } -func firebaseServiceAccountFile(t *testing.T) string { - if 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") != "" { - filename := filepath.Join(t.TempDir(), "firebase.json") - require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0o600)) - return filename - } - t.SkipNow() - return "" -} - func basicAuth(s string) string { return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(s))) } From 769e071593c8203190bcb310ee2270d0e25d1ffa Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 23:27:24 -0400 Subject: [PATCH 12/38] Refining, changelog --- docs/releases.md | 4 ++++ server/config.go | 26 +++++++++++++------------- server/errors.go | 1 - server/smtp_server.go | 1 + server/visitor.go | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 4131a89e..e4f78db9 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -13,6 +13,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## ntfy server v1.25.0 (UNRELEASED) +**Bugs**: + +* Respect Firebase "quota exceeded" response for topics, block Firebase publishing for user for 10min ([#289](https://github.com/binwiederhier/ntfy/issues/289)) + **Maintenance:** * Upgrade Firebase Admin SDK to 4.x ([#274](https://github.com/binwiederhier/ntfy/issues/274)) diff --git a/server/config.go b/server/config.go index 7f15aedc..60d3cdf9 100644 --- a/server/config.go +++ b/server/config.go @@ -6,16 +6,16 @@ import ( // Defines default config settings (excluding limits, see below) const ( - DefaultListenHTTP = ":80" - DefaultCacheDuration = 12 * time.Hour - DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) - DefaultManagerInterval = time.Minute - DefaultAtSenderInterval = 10 * time.Second - DefaultMinDelay = 10 * time.Second - DefaultMaxDelay = 3 * 24 * time.Hour - 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) - DefaultFirebaseQuotaLimitPenaltyDuration = 10 * time.Minute + DefaultListenHTTP = ":80" + DefaultCacheDuration = 12 * time.Hour + DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!) + DefaultManagerInterval = time.Minute + DefaultDelayedSenderInterval = 10 * time.Second + DefaultMinDelay = 10 * time.Second + DefaultMaxDelay = 3 * 24 * time.Hour + 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) + DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded" ) // Defines all global and per-visitor limits @@ -70,7 +70,7 @@ type Config struct { DelayedSenderInterval time.Duration FirebaseKeepaliveInterval time.Duration FirebasePollInterval time.Duration - FirebaseQuotaLimitPenaltyDuration time.Duration + FirebaseQuotaExceededPenaltyDuration time.Duration UpstreamBaseURL string SMTPSenderAddr string SMTPSenderUser string @@ -120,10 +120,10 @@ func NewConfig() *Config { MessageLimit: DefaultMessageLengthLimit, MinDelay: DefaultMinDelay, MaxDelay: DefaultMaxDelay, - DelayedSenderInterval: DefaultAtSenderInterval, + DelayedSenderInterval: DefaultDelayedSenderInterval, FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval, FirebasePollInterval: DefaultFirebasePollInterval, - FirebaseQuotaLimitPenaltyDuration: DefaultFirebaseQuotaLimitPenaltyDuration, + FirebaseQuotaExceededPenaltyDuration: DefaultFirebaseQuotaExceededPenaltyDuration, TotalTopicLimit: DefaultTotalTopicLimit, VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit, VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit, diff --git a/server/errors.go b/server/errors.go index 2fa883fa..32c1b3b9 100644 --- a/server/errors.go +++ b/server/errors.go @@ -59,7 +59,6 @@ var ( errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} - errHTTPTooManyRequestsFirebaseQuotaReached = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: Firebase quota for topic reached", "https://ntfy.sh/docs/publish/#limitations"} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""} ) diff --git a/server/smtp_server.go b/server/smtp_server.go index a5b6f850..7812371e 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -146,6 +146,7 @@ func (s *smtpSession) publishMessage(m *message) error { url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message)) req.RemoteAddr = s.remoteAddr // rate limiting!! + req.Header.Set("X-Forwarded-For", s.remoteAddr) if err != nil { return err } diff --git a/server/visitor.go b/server/visitor.go index 1bbc4e00..5a8e186b 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -73,7 +73,7 @@ func (v *visitor) FirebaseAllowed() error { func (v *visitor) FirebaseTemporarilyDeny() { v.mu.Lock() defer v.mu.Unlock() - v.firebase = time.Now().Add(v.config.FirebaseQuotaLimitPenaltyDuration) + v.firebase = time.Now().Add(v.config.FirebaseQuotaExceededPenaltyDuration) } func (v *visitor) EmailAllowed() error { From 9202d8553217f48931c3b1a4e33eb2e37a8aacfe Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 23:36:06 -0400 Subject: [PATCH 13/38] Make linter happy --- server/server_firebase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server_firebase.go b/server/server_firebase.go index 47d27555..b34348d6 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -20,7 +20,7 @@ const ( ) var ( - errFirebaseQuotaExceeded = errors.New("Firebase quota exceeded") + errFirebaseQuotaExceeded = errors.New("quota exceeded for Firebase messages to topic") ) // firebaseClient is a generic client that formats and sends messages to Firebase. From 8c32f029fbb1345435f37f2feb3c20b1b237e2e5 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 31 May 2022 23:55:05 -0400 Subject: [PATCH 14/38] Fix data races --- docs/releases.md | 1 + server/server_firebase_test.go | 23 +++++++++++++++++++---- server/server_test.go | 8 ++++---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index e4f78db9..022d5f9a 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -25,6 +25,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Documentation**: * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) +* Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) --> diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 8e08b0d6..a458f2ec 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "heckel.io/ntfy/auth" "strings" + "sync" "testing" ) @@ -29,6 +30,7 @@ func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error { type testFirebaseSender struct { allowed int messages []*messaging.Message + mu sync.Mutex } func newTestFirebaseSender(allowed int) *testFirebaseSender { @@ -37,7 +39,10 @@ func newTestFirebaseSender(allowed int) *testFirebaseSender { messages: make([]*messaging.Message, 0), } } + func (s *testFirebaseSender) Send(m *messaging.Message) error { + s.mu.Lock() + defer s.mu.Unlock() if len(s.messages)+1 > s.allowed { return errFirebaseQuotaExceeded } @@ -45,6 +50,16 @@ func (s *testFirebaseSender) Send(m *messaging.Message) error { return nil } +func (s *testFirebaseSender) Messages() []*messaging.Message { + s.mu.Lock() + defer s.mu.Unlock() + messages := make([]*messaging.Message, 0) + for _, m := range s.messages { + messages = append(messages, m) + } + return messages +} + func TestToFirebaseMessage_Keepalive(t *testing.T) { m := newKeepaliveMessage("mytopic") fbm, err := toFirebaseMessage(m, nil) @@ -311,15 +326,15 @@ func TestToFirebaseSender_Abuse(t *testing.T) { visitor := newVisitor(newTestConfig(t), newMemTestCache(t), "1.2.3.4") require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 1, len(sender.messages)) + require.Equal(t, 1, len(sender.Messages())) require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 2, len(sender.messages)) + require.Equal(t, 2, len(sender.Messages())) require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 2, len(sender.messages)) + require.Equal(t, 2, len(sender.Messages())) sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 0, len(sender.messages)) + require.Equal(t, 0, len(sender.Messages())) } diff --git a/server/server_test.go b/server/server_test.go index d05075fd..ce63f272 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -63,10 +63,10 @@ func TestServer_PublishWithFirebase(t *testing.T) { msg1 := toMessage(t, response.Body.String()) require.NotEmpty(t, msg1.ID) require.Equal(t, "my first message", msg1.Message) - require.Equal(t, 1, len(sender.messages)) - require.Equal(t, "my first message", sender.messages[0].Data["message"]) - require.Equal(t, "my first message", sender.messages[0].APNS.Payload.Aps.Alert.Body) - require.Equal(t, "my first message", sender.messages[0].APNS.Payload.CustomData["message"]) + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) } func TestServer_SubscribeOpenAndKeepalive(t *testing.T) { From ad55de784d76b756de7a3f4d8b630506659805aa Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Jun 2022 00:03:56 -0400 Subject: [PATCH 15/38] Add Chinese translation --- docs/releases.md | 4 ++++ web/src/components/Preferences.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 022d5f9a..637fea3f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -27,6 +27,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) * Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) +**Additional translations:** + +* Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/)) + --> diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index b93702fa..e2899ecc 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -436,7 +436,7 @@ const Appearance = () => { const Language = () => { const { t, i18n } = useTranslation(); const labelId = "prefLanguage"; - const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇹", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); + const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" "); const lang = i18n.language ?? "en"; @@ -452,6 +452,7 @@ const Language = () => { Bahasa Indonesia Български Čeština + 中文 Deutsch Español Français From 17eabed11c453b8db5c03245d0451dd4deb4fe86 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Jun 2022 08:56:50 -0400 Subject: [PATCH 16/38] Clarify wording for iOS push notifications --- docs/config.md | 12 ++++++++++-- docs/releases.md | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 15b547bb..9b0aa07c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -643,10 +643,18 @@ 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 + poll request to `https://ntfy.sh/6de73be8dfb7d69e...`. The request from your server to the upstream server + contains only the message ID (in the `X-Poll-ID` header), and the SHA256 checksum of the topic URL (as upstream topic). +- The ntfy.sh server publishes the poll request 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 +Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl: + +``` +curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b +{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"} +``` + ## Rate limiting !!! info Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag. diff --git a/docs/releases.md b/docs/releases.md index 637fea3f..1a3fa4d6 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -26,6 +26,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) * Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) +* Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting) **Additional translations:** From eb0847392c81c81f4452c1960f15eda9cb98cb7d Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Jun 2022 09:05:21 -0400 Subject: [PATCH 17/38] Fix staticcheck --- server/server_firebase_test.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index a458f2ec..b29cf3af 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -53,11 +53,7 @@ func (s *testFirebaseSender) Send(m *messaging.Message) error { func (s *testFirebaseSender) Messages() []*messaging.Message { s.mu.Lock() defer s.mu.Unlock() - messages := make([]*messaging.Message, 0) - for _, m := range s.messages { - messages = append(messages, m) - } - return messages + return append(make([]*messaging.Message, 0), s.messages...) } func TestToFirebaseMessage_Keepalive(t *testing.T) { From b9e507939985cd90eb208df86a6f65594e5c3267 Mon Sep 17 00:00:00 2001 From: "Philipp C. Heckel" Date: Wed, 1 Jun 2022 13:05:38 -0400 Subject: [PATCH 18/38] Update README.md --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 94ac1d4f..f6969459 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ too. [Install / Self-hosting](https://ntfy.sh/docs/install/) | [Building](https://ntfy.sh/docs/develop/) +## Chat +You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) +(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information +[on my website](https://heckel.io/about). + +## Announcements / beta testers +For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) +topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, +join Discord/Matrix (I'll eventually make a testing channel in Google Play). + ## Contributing I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app. @@ -43,11 +53,6 @@ Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start im Translation status -## Contact me -You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) -(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information -[on my website](https://heckel.io/about). - ## License Made with ❤️ by [Philipp C. Heckel](https://heckel.io). The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2). From 41fd8454cf4b073f8055d0c8c13e2606ac20f696 Mon Sep 17 00:00:00 2001 From: SchoNie Date: Tue, 31 May 2022 18:00:32 +0000 Subject: [PATCH 19/38] Translated using Weblate (Dutch) Currently translated at 100.0% (189 of 189 strings) Translation: ntfy/Web app Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nl/ --- web/public/static/langs/nl.json | 190 +++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 3 deletions(-) diff --git a/web/public/static/langs/nl.json b/web/public/static/langs/nl.json index 56fdf389..11954852 100644 --- a/web/public/static/langs/nl.json +++ b/web/public/static/langs/nl.json @@ -1,7 +1,191 @@ { "action_bar_settings": "Instellingen", - "action_bar_send_test_notification": "Stuur testmelding", - "action_bar_clear_notifications": "Alle meldingen wissen", + "action_bar_send_test_notification": "Stuur test notificatie", + "action_bar_clear_notifications": "Wis alle notificaties", "message_bar_type_message": "Typ hier een bericht", - "action_bar_unsubscribe": "Afmelden" + "action_bar_unsubscribe": "Afmelden", + "message_bar_error_publishing": "Fout bij publiceren notificatie", + "nav_topics_title": "Geabonneerde onderwerpen", + "nav_button_settings": "Instellingen", + "alert_not_supported_description": "Notificaties worden niet ondersteund in je browser.", + "notifications_none_for_any_title": "Je hebt nog geen notificaties ontvangen.", + "publish_dialog_tags_label": "Tags", + "publish_dialog_chip_attach_file_label": "Lokaal bestand bijvoegen", + "prefs_users_dialog_title_edit": "Gebruiker bewerken", + "error_boundary_title": "Oh nee, ntfy is vastgelopen", + "error_boundary_description": "Dit hoort natuurlijk niet te gebeuren. Onze excuses.
Wanneer het mogelijk is, meld deze fout op GitHub, of laat het ons weten via Discord of Matrix.", + "error_boundary_button_copy_stack_trace": "Stack trace kopiëren", + "error_boundary_stack_trace": "Stacktrace", + "error_boundary_gathering_info": "Meer informatie verzamelen …", + "prefs_users_delete_button": "Gebruiker verwijderen", + "prefs_notifications_delete_after_one_week": "Na één week", + "prefs_notifications_delete_after_one_month": "Na één maand", + "prefs_users_dialog_title_add": "Gebruiker toevoegen", + "prefs_users_dialog_password_label": "Wachtwoord", + "error_boundary_unsupported_indexeddb_description": "De ntfy web applicatie heeft IndexedDB nodig om correct te kunnen functioneren, helaas ondersteund jouw browser IndexedDB niet in privé / incognito modus.

Dit is jammer maar het is ook onlogisch om de ntfy web applicatie in privé / incognito modus te gebruiken want alle gegevens worden bewaard in de browser zijn lokale opslag. Je kan hier meer over lezen in deze GitHub issue, of praat met ons op Discord of Matrix.", + "action_bar_show_menu": "Toon menu", + "action_bar_logo_alt": "ntfy logo", + "action_bar_toggle_mute": "Notificaties dempen/opheffen", + "action_bar_toggle_action_menu": "Actie menu openen/sluiten", + "message_bar_show_dialog": "Toon publicatie venster", + "message_bar_publish": "Bericht publiceren", + "nav_button_all_notifications": "Alle notificaties", + "nav_button_documentation": "Documentatie", + "nav_button_publish_message": "Notificatie publiceren", + "nav_button_subscribe": "Onderwerp abonneren", + "nav_button_muted": "Notificaties gedempt", + "nav_button_connecting": "verbinden", + "alert_grant_title": "Notificaties zijn uitgeschakeld", + "alert_grant_description": "Geef je browser toestemming om meldingen weer te geven.", + "alert_grant_button": "Nu toestaan", + "alert_not_supported_title": "Notificaties zijn niet ondersteund", + "notifications_list": "Notificaties lijst", + "notifications_list_item": "Notificatie", + "notifications_mark_read": "Markeer als gelezen", + "notifications_delete": "Verwijder", + "notifications_copied_to_clipboard": "Gekopieerd naar klembord", + "notifications_tags": "Tags", + "notifications_priority_x": "Prioriteit {{priority}}", + "notifications_new_indicator": "Nieuwe notificatie", + "notifications_attachment_image": "Afbeelding bijlage", + "notifications_attachment_copy_url_title": "Kopieer URL van bijlage naar klembord", + "notifications_attachment_copy_url_button": "URL kopiëren", + "notifications_attachment_open_title": "Ga naar {{url}}", + "notifications_attachment_open_button": "Bijlage openen", + "notifications_attachment_link_expires": "link vervalt op {{date}}", + "notifications_attachment_link_expired": "download link is verlopen", + "notifications_attachment_file_image": "afbeeldingsbestand", + "notifications_attachment_file_video": "videobestand", + "notifications_attachment_file_audio": "audiobestand", + "notifications_attachment_file_app": "Android app bestand", + "notifications_attachment_file_document": "overig document", + "notifications_click_copy_url_title": "URL naar klembord kopiëren", + "notifications_click_copy_url_button": "Link kopiëren", + "notifications_click_open_button": "Link openen", + "notifications_none_for_topic_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar het onderwerp URL.", + "notifications_none_for_any_description": "Om notificaties naar dit onderwerp te sturen, doe een PUT of POST naar het onderwerp URL. Hier is een voorbeeld met één van je onderwerpen.", + "notifications_no_subscriptions_title": "Het lijkt erop dat je nog op geen onderwerpen geabonneerd bent.", + "notifications_no_subscriptions_description": "Klik op de \"{{linktext}}\" link om een onderwerp te maken of erop te abonneren. Daarna kan je berichten sturen via PUT of POST and ontvang je hier notificaties.", + "notifications_example": "Voorbeeld", + "notifications_more_details": "Voor meer informatie, bezoek de website of documentatie.", + "notifications_loading": "Notificaties laden …", + "publish_dialog_title_topic": "Publiceren naar {{topic}}", + "publish_dialog_title_no_topic": "Notificatie publiceren", + "publish_dialog_progress_uploading": "Uploaden …", + "notifications_actions_open_url_title": "Ga naar {{url}}", + "notifications_actions_not_supported": "Deze actie is niet ondersteund in de web applicatie", + "notifications_actions_http_request_title": "Stuur HTTP {{method}} naar {{url}}", + "notifications_none_for_topic_title": "Je hebt nog geen notificaties ontvangen voor dit onderwerp.", + "publish_dialog_priority_low": "Lage prioriteit", + "publish_dialog_progress_uploading_detail": "Uploaden {{loaded}}/{{total}} ({{percent}}%) …", + "publish_dialog_message_published": "Notificatie gepubliceerd", + "publish_dialog_attachment_limits_file_and_quota_reached": "overschrijd {{fileSizeLimit}} bestandslimiet en quotum, {{remainingBytes}} resterend", + "publish_dialog_attachment_limits_file_reached": "overschrijd {{fileSizeLimit}} bestandslimiet", + "publish_dialog_priority_default": "Standaard prioriteit", + "publish_dialog_attachment_limits_quota_reached": "overschrijd quotum, {{remainingBytes}} resterend", + "publish_dialog_emoji_picker_show": "Kies een emoji", + "publish_dialog_priority_high": "Hoge prioriteit", + "publish_dialog_priority_max": "Maximale prioriteit", + "publish_dialog_priority_min": "Minimale prioriteit", + "publish_dialog_base_url_label": "Service URL", + "publish_dialog_base_url_placeholder": "Service URL, bijvoorbeeld: https://voorbeeld.com", + "publish_dialog_topic_label": "Onderwerp", + "publish_dialog_topic_placeholder": "Onderwerp, bijv. phil_alerts", + "publish_dialog_topic_reset": "Onderwerp resetten", + "publish_dialog_title_label": "Titel", + "publish_dialog_title_placeholder": "Notificatie titel , bijv. Schijfruimte alarm", + "publish_dialog_message_label": "Bericht", + "publish_dialog_message_placeholder": "Typ hier een bericht", + "publish_dialog_tags_placeholder": "Komma gescheiden lijst met tags, bijv. waarschuwing, srv1-backup", + "publish_dialog_priority_label": "Prioriteit", + "publish_dialog_click_label": "Klik URL", + "publish_dialog_click_reset": "Verwijder klik URL", + "publish_dialog_email_label": "Email", + "publish_dialog_email_placeholder": "Adres om de notificatie naar door te sturen, bijv. phil@voorbeeld.com", + "publish_dialog_email_reset": "Email doorsturen verwijderen", + "publish_dialog_attach_label": "URL van bijlage", + "publish_dialog_click_placeholder": "URL die geopend zal worden wanneer op de notificatie geklikt wordt", + "publish_dialog_attach_placeholder": "Bestand bijvoegen via URL, bijv. https://f-droid.org/F-Droid.apk", + "publish_dialog_attach_reset": "Bijlage URL verwijderen", + "publish_dialog_filename_label": "Bestandsnaam", + "publish_dialog_filename_placeholder": "Bestandsnaam van bijlage", + "publish_dialog_delay_label": "Uitstellen", + "publish_dialog_delay_placeholder": "Bezorging uitstellen, bijv. {{unixTimestamp}}, {{relativeTime}}, of \"{{naturalLanguage}}\" (alleen Engels)", + "publish_dialog_delay_reset": "Verwijder uitgestelde bezorging", + "publish_dialog_other_features": "Andere functionaliteiten:", + "publish_dialog_chip_click_label": "Klik URL", + "publish_dialog_chip_email_label": "Doorsturen naar email", + "publish_dialog_chip_attach_url_label": "Bestand bijvoegen via URL", + "publish_dialog_chip_delay_label": "Uitgestelde bezorging", + "publish_dialog_chip_topic_label": "Onderwerp veranderen", + "publish_dialog_details_examples_description": "Voor meer voorbeelden en gedetailleerde beschrijvingen van alle functionaliteiten, bekijk de documentatie.", + "publish_dialog_button_cancel_sending": "Versturen annuleren", + "publish_dialog_button_cancel": "Annuleer", + "publish_dialog_button_send": "Verstuur", + "publish_dialog_checkbox_publish_another": "Nog een bericht versturen", + "publish_dialog_attached_file_title": "Bijgevoegd bestand:", + "publish_dialog_attached_file_filename_placeholder": "Bijlage bestandsnaam", + "publish_dialog_attached_file_remove": "Verwijder bijgevoegd bestand", + "publish_dialog_drop_file_here": "Bestand hier slepen", + "emoji_picker_search_placeholder": "Emoji zoeken", + "emoji_picker_search_clear": "Zoeken leegmaken", + "subscribe_dialog_subscribe_topic_placeholder": "Onderwerp naam, bijv. phils_waarschuwingen", + "subscribe_dialog_subscribe_use_another_label": "Gebruik een andere server", + "subscribe_dialog_subscribe_base_url_label": "Service URL", + "subscribe_dialog_subscribe_button_cancel": "Annuleren", + "subscribe_dialog_subscribe_button_subscribe": "Abonneren", + "subscribe_dialog_login_title": "Aanmelding vereist", + "subscribe_dialog_login_description": "Dit onderwerp is beveiligd met een wachtwoord. Geef een gebruikersnaam en wachtwoord op om te abonneren.", + "subscribe_dialog_login_username_label": "Gebruikersnaam, bijv. phil", + "subscribe_dialog_subscribe_title": "Onderwerp abonneren", + "subscribe_dialog_subscribe_description": "Onderwerpen zijn mogelijk niet beschermd met een wachtwoord, kies daarom een moeilijk te raden naam. Na abonneren kun je notificaties via PUT/POST sturen.", + "subscribe_dialog_login_password_label": "Wachtwoord", + "subscribe_dialog_login_button_back": "Terug", + "subscribe_dialog_login_button_login": "Aanmelden", + "subscribe_dialog_error_user_not_authorized": "Gebruiker {{username}} heeft geen toegang", + "subscribe_dialog_error_user_anonymous": "anoniem", + "prefs_notifications_title": "Notificaties", + "prefs_notifications_sound_title": "Meldingsgeluid", + "prefs_notifications_sound_description_none": "Notificaties zullen geen geluid geven", + "prefs_notifications_sound_play": "Geselecteerd geluid afspelen", + "prefs_notifications_sound_description_some": "Inkomende notificaties zullen het {{sound}} afspelen", + "prefs_notifications_sound_no_sound": "Geen geluid", + "prefs_notifications_min_priority_title": "Minimale prioriteit", + "prefs_notifications_min_priority_description_any": "Toon alle notificaties, ongeacht prioriteit", + "prefs_notifications_min_priority_description_x_or_higher": "Toon notificaties als prioriteit is {{number}} ({{name}}) of hoger", + "prefs_notifications_min_priority_description_max": "Toon notificaties als prioriteit is 5 (maximaal)", + "prefs_notifications_min_priority_any": "Elke prioriteit", + "prefs_notifications_min_priority_low_and_higher": "Lage prioriteit en hoger", + "prefs_notifications_min_priority_default_and_higher": "Standaard prioriteit en hoger", + "prefs_notifications_min_priority_high_and_higher": "Hoge prioriteit en hoger", + "prefs_notifications_min_priority_max_only": "Alleen maximale prioriteit", + "prefs_notifications_delete_after_title": "Notificaties verwijderen", + "prefs_notifications_delete_after_never": "Nooit", + "prefs_notifications_delete_after_three_hours": "Na drie uur", + "prefs_notifications_delete_after_one_day": "Na één dag", + "prefs_notifications_delete_after_never_description": "Notificaties worden nooit automatisch verwijderd", + "prefs_notifications_delete_after_three_hours_description": "Notificaties worden na drie uur automatisch verwijderd", + "prefs_notifications_delete_after_one_day_description": "Notificaties worden na één dag automatisch verwijderd", + "prefs_notifications_delete_after_one_week_description": "Notificaties worden na één week automatisch verwijderd", + "prefs_notifications_delete_after_one_month_description": "Notificaties worden na één maand automatisch verwijderd", + "prefs_users_title": "Gebruikers beheren", + "prefs_users_description": "Gebruikers voor beveiligde onderwerpen kunnen hier toegevoegd of verwijderd worden. Let op: gebruikersnaam en wachtwoord worden opgeslagen in lokale browser opslag.", + "prefs_users_table": "Gebruikerstabel", + "prefs_users_add_button": "Gebruiker toevoegen", + "prefs_users_edit_button": "Gebruiker bewerken", + "prefs_users_table_user_header": "Gebruiker", + "prefs_users_table_base_url_header": "Service URL", + "prefs_users_dialog_base_url_label": "Service URL, bijv. https://ntfy.sh", + "prefs_users_dialog_username_label": "Gebruikersnaam, bijv. phil", + "prefs_users_dialog_button_cancel": "Annuleren", + "prefs_users_dialog_button_add": "Toevoegen", + "prefs_users_dialog_button_save": "Bewaren", + "prefs_appearance_title": "Weergave", + "prefs_appearance_language_title": "Taal", + "priority_min": "min", + "priority_low": "laag", + "priority_default": "standaard", + "priority_high": "hoog", + "priority_max": "max", + "error_boundary_unsupported_indexeddb_title": "Privé / incognito browservensters worden niet ondersteund" } From ab955d4d1c7bfbc8f492f63bd0a0d610c697e1b7 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Jun 2022 16:57:35 -0400 Subject: [PATCH 20/38] Logging --- cmd/access.go | 2 +- cmd/app.go | 24 +++++-------- cmd/config_loader.go | 12 +++++-- cmd/publish.go | 2 +- cmd/serve.go | 45 +++++++++++++++++++++--- cmd/subscribe.go | 8 ++--- cmd/subscribe_darwin.go | 4 +-- cmd/subscribe_linux.go | 4 +-- cmd/subscribe_windows.go | 2 +- cmd/user.go | 2 +- log/log.go | 26 ++++++++++++-- server/ntfy.service | 1 + server/server.go | 76 +++++++++++++++++++++++++++------------- server/server.yml | 5 +++ server/topic.go | 13 ++++--- 15 files changed, 161 insertions(+), 65 deletions(-) diff --git a/cmd/access.go b/cmd/access.go index b36dc38b..e1f61bc1 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -28,7 +28,7 @@ var cmdAccess = &cli.Command{ Usage: "Grant/revoke access to a topic, or show access", UsageText: "ntfy access [USERNAME [TOPIC [PERMISSION]]]", Flags: flagsAccess, - Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsAccess)), + Before: initConfigFileInputSourceFunc("config", flagsAccess, initLogFunc), Action: execUserAccess, Category: categoryServer, Description: `Manage the access control list for the ntfy server. diff --git a/cmd/app.go b/cmd/app.go index 89634a8b..adac9d73 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" "heckel.io/ntfy/log" "os" ) @@ -16,7 +17,7 @@ var commands = make([]*cli.Command, 0) var flagsDefault = []cli.Flag{ &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, - &cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}, + altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}), } // New creates a new CLI application @@ -32,22 +33,15 @@ func New() *cli.App { ErrWriter: os.Stderr, Commands: commands, Flags: flagsDefault, - Before: initLogFunc(nil), + Before: initLogFunc, } } -func initLogFunc(next cli.BeforeFunc) cli.BeforeFunc { - return func(c *cli.Context) error { - if c.Bool("debug") { - log.SetLevel(log.DebugLevel) - } else { - log.SetLevel(log.ToLevel(c.String("log-level"))) - } - if next != nil { - if err := next(c); err != nil { - return err - } - } - return nil +func initLogFunc(c *cli.Context) error { + if c.Bool("debug") { + log.SetLevel(log.DebugLevel) + } else { + log.SetLevel(log.ToLevel(c.String("log-level"))) } + return nil } diff --git a/cmd/config_loader.go b/cmd/config_loader.go index 7840c6e7..6d984840 100644 --- a/cmd/config_loader.go +++ b/cmd/config_loader.go @@ -11,7 +11,7 @@ import ( // 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 { +func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag, next cli.BeforeFunc) cli.BeforeFunc { return func(context *cli.Context) error { configFile := context.String(configFlag) if context.IsSet(configFlag) && !util.FileExists(configFile) { @@ -23,7 +23,15 @@ func initConfigFileInputSourceFunc(configFlag string, flags []cli.Flag) cli.Befo if err != nil { return err } - return altsrc.ApplyInputSourceValues(context, inputSource, flags) + if err := altsrc.ApplyInputSourceValues(context, inputSource, flags); err != nil { + return err + } + if next != nil { + if err := next(context); err != nil { + return err + } + } + return nil } } diff --git a/cmd/publish.go b/cmd/publish.go index 51d30b6a..c56aecad 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -44,7 +44,7 @@ var cmdPublish = &cli.Command{ Action: execPublish, Category: categoryClient, Flags: flagsPublish, - Before: initLogFunc(nil), + Before: initLogFunc, Description: `Publish a message to a ntfy server. Examples: diff --git a/cmd/serve.go b/cmd/serve.go index df1f5798..50969e03 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -8,7 +8,10 @@ import ( "heckel.io/ntfy/log" "math" "net" + "os" + "os/signal" "strings" + "syscall" "time" "github.com/urfave/cli/v2" @@ -21,9 +24,13 @@ func init() { commands = append(commands, cmdServe) } +const ( + defaultServerConfigFile = "/etc/ntfy/server.yml" +) + var flagsServe = append( flagsDefault, - &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"}, + &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}), @@ -69,7 +76,7 @@ var cmdServe = &cli.Command{ Action: execServe, Category: categoryServer, Flags: flagsServe, - Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsServe)), + Before: initConfigFileInputSourceFunc("config", flagsServe, initLogFunc), Description: `Run the ntfy server and listen for incoming requests The command will load the configuration from /etc/ntfy/server.yml. Config options can @@ -86,6 +93,7 @@ func execServe(c *cli.Context) error { } // Read all the options + config := c.String("config") baseURL := c.String("base-url") listenHTTP := c.String("listen-http") listenHTTPS := c.String("listen-https") @@ -241,11 +249,15 @@ func execServe(c *cli.Context) error { conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish conf.BehindProxy = behindProxy conf.EnableWeb = enableWeb + + // Set up hot-reloading of config + go sigHandlerConfigReload(config) + + // Run server s, err := server.New(conf) if err != nil { log.Fatal(err) - } - if err := s.Run(); err != nil { + } else if err := s.Run(); err != nil { log.Fatal(err) } log.Info("Exiting.") @@ -262,3 +274,28 @@ func parseSize(s string, defaultValue int64) (v int64, err error) { } return v, nil } + +func sigHandlerConfigReload(config string) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGHUP) + for range sigs { + log.Info("Partially hot reloading configuration ...") + inputSource, err := newYamlSourceFromFile(config, flagsServe) + if err != nil { + log.Warn("Hot reload failed: %s", err.Error()) + continue + } + reloadLogLevel(inputSource) + } +} + +func reloadLogLevel(inputSource altsrc.InputSourceContext) { + newLevelStr, err := inputSource.String("log-level") + if err != nil { + log.Warn("Cannot load log level: %s", err.Error()) + return + } + newLevel := log.ToLevel(newLevelStr) + log.SetLevel(newLevel) + log.Info("Log level is %s", newLevel.String()) +} diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 618cdb9b..3b469344 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -43,7 +43,7 @@ var cmdSubscribe = &cli.Command{ Action: execSubscribe, Category: categoryClient, Flags: flagsSubscribe, - Before: initLogFunc(nil), + Before: initLogFunc, Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for every arriving message. There are 3 modes in which the command can be run: @@ -253,7 +253,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { if filename != "" { return client.LoadConfig(filename) } - configFile := defaultConfigFile() + configFile := defaultClientConfigFile() if s, _ := os.Stat(configFile); s != nil { return client.LoadConfig(configFile) } @@ -261,7 +261,7 @@ func loadConfig(c *cli.Context) (*client.Config, error) { } //lint:ignore U1000 Conditionally used in different builds -func defaultConfigFileUnix() string { +func defaultClientConfigFileUnix() string { u, _ := user.Current() configFile := clientRootConfigFileUnixAbsolute if u.Uid != "0" { @@ -272,7 +272,7 @@ func defaultConfigFileUnix() string { } //lint:ignore U1000 Conditionally used in different builds -func defaultConfigFileWindows() string { +func defaultClientConfigFileWindows() string { homeDir, _ := os.UserConfigDir() return filepath.Join(homeDir, clientUserConfigFileWindowsRelative) } diff --git a/cmd/subscribe_darwin.go b/cmd/subscribe_darwin.go index e4f44ed6..0372a79f 100644 --- a/cmd/subscribe_darwin.go +++ b/cmd/subscribe_darwin.go @@ -11,6 +11,6 @@ var ( scriptLauncher = []string{"sh", "-c"} ) -func defaultConfigFile() string { - return defaultConfigFileUnix() +func defaultClientConfigFile() string { + return defaultClientConfigFileUnix() } diff --git a/cmd/subscribe_linux.go b/cmd/subscribe_linux.go index c57660e8..346606bd 100644 --- a/cmd/subscribe_linux.go +++ b/cmd/subscribe_linux.go @@ -11,6 +11,6 @@ var ( scriptLauncher = []string{"sh", "-c"} ) -func defaultConfigFile() string { - return defaultConfigFileUnix() +func defaultClientConfigFile() string { + return defaultClientConfigFileUnix() } diff --git a/cmd/subscribe_windows.go b/cmd/subscribe_windows.go index 1d5c6655..129e8f52 100644 --- a/cmd/subscribe_windows.go +++ b/cmd/subscribe_windows.go @@ -11,5 +11,5 @@ var ( ) func defaultConfigFile() string { - return defaultConfigFileWindows() + return defaultClientConfigFileWindows() } diff --git a/cmd/user.go b/cmd/user.go index 921aeda1..acc06d4c 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -29,7 +29,7 @@ var cmdUser = &cli.Command{ Usage: "Manage/show users", UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...", Flags: flagsUser, - Before: initLogFunc(initConfigFileInputSourceFunc("config", flagsUser)), + Before: initConfigFileInputSourceFunc("config", flagsUser, initLogFunc), Category: categoryServer, Subcommands: []*cli.Command{ { diff --git a/log/log.go b/log/log.go index 8c134508..36abc0e3 100644 --- a/log/log.go +++ b/log/log.go @@ -3,10 +3,13 @@ package log import ( "log" "strings" + "sync" ) +// Level is a well-known log level, as defined below type Level int +// Well known log levels const ( DebugLevel Level = iota InfoLevel @@ -30,32 +33,50 @@ func (l Level) String() string { var ( level = InfoLevel + mu = &sync.Mutex{} ) +// Debug prints the given message, if the current log level is DEBUG func Debug(message string, v ...interface{}) { logIf(DebugLevel, message, v...) } +// Info prints the given message, if the current log level is INFO or lower func Info(message string, v ...interface{}) { logIf(InfoLevel, message, v...) } +// Warn prints the given message, if the current log level is WARN or lower func Warn(message string, v ...interface{}) { logIf(WarnLevel, message, v...) } +// Error prints the given message, if the current log level is ERROR or lower func Error(message string, v ...interface{}) { logIf(ErrorLevel, message, v...) } +// Fatal prints the given message, and exits the program func Fatal(v ...interface{}) { log.Fatalln(v...) } +// CurrentLevel returns the current log level +func CurrentLevel() Level { + mu.Lock() + defer mu.Unlock() + return level +} + +// SetLevel sets a new log level func SetLevel(newLevel Level) { + mu.Lock() + defer mu.Unlock() level = newLevel } +// ToLevel converts a string to a Level. It returns InfoLevel if the string +// does not match any known log levels. func ToLevel(s string) Level { switch strings.ToLower(s) { case "debug": @@ -67,13 +88,12 @@ func ToLevel(s string) Level { case "error": return ErrorLevel default: - log.Fatalf("unknown log level: %s", s) - return 0 + return InfoLevel } } func logIf(l Level, message string, v ...interface{}) { - if level <= l { + if CurrentLevel() <= l { log.Printf(l.String()+" "+message, v...) } } diff --git a/server/ntfy.service b/server/ntfy.service index 6645b21f..f32ed898 100644 --- a/server/ntfy.service +++ b/server/ntfy.service @@ -6,6 +6,7 @@ After=network.target User=ntfy Group=ntfy ExecStart=/usr/bin/ntfy serve +ExecReload=/bin/kill --signal HUP $MAINPID Restart=on-failure AmbientCapabilities=CAP_NET_BIND_SERVICE LimitNOFILE=10000 diff --git a/server/server.go b/server/server.go index 8ed5795c..4688a03f 100644 --- a/server/server.go +++ b/server/server.go @@ -179,7 +179,7 @@ func (s *Server) Run() error { if s.config.SMTPServerListen != "" { listenStr += fmt.Sprintf(" %s[smtp]", s.config.SMTPServerListen) } - log.Info("Listening on%s", listenStr) + log.Info("Listening on%s, log level is %s", listenStr, log.CurrentLevel().String()) mux := http.NewServeMux() mux.HandleFunc("/", s.handle) errChan := make(chan error) @@ -246,18 +246,28 @@ func (s *Server) Stop() { func (s *Server) handle(w http.ResponseWriter, r *http.Request) { v := s.visitor(r) - log.Debug("[%s] %s %s", v.ip, r.Method, r.URL.Path) - + log.Debug("%s HTTP %s %s", v.ip, r.Method, r.URL.Path) if err := s.handleInternal(w, r, v); err != nil { if websocket.IsWebSocketUpgrade(r) { - log.Info("[%s] WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + isNormalError := websocket.IsCloseError(err, websocket.CloseAbnormalClosure) || strings.Contains(err.Error(), "i/o timeout") + if isNormalError { + log.Debug("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + } else { + log.Warn("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + } return // Do not attempt to write to upgraded connection } httpErr, ok := err.(*errHTTP) if !ok { httpErr = errHTTPInternalError } - log.Info("[%s] HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + isNormalError := httpErr.Code == 404 + if isNormalError { + log.Debug("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + } else { + log.Info("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + } + w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.WriteHeader(httpErr.HTTPCode) @@ -434,21 +444,23 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito m.Message = emptyMessageBody } delayed := m.Time > time.Now().Unix() - log.Debug("[%s] %s %s: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", - v.ip, r.Method, r.URL.Path, m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) + log.Debug("%s Received message: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", + logPrefix(v, m), m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) if !delayed { if err := t.Publish(v, m); err != nil { return err } - } - if s.firebaseClient != nil && firebase && !delayed { - go s.sendToFirebase(v, m) - } - if s.mailer != nil && email != "" && !delayed { - go s.sendEmail(v, m, email) - } - if s.config.UpstreamBaseURL != "" && !delayed { - go s.forwardPollRequest(v, m) + if s.firebaseClient != nil && firebase { + go s.sendToFirebase(v, m) + } + if s.mailer != nil && email != "" { + go s.sendEmail(v, m, email) + } + if s.config.UpstreamBaseURL != "" { + go s.forwardPollRequest(v, m) + } + } else { + log.Debug("%s Message delayed, will process later", logPrefix(v, m)) } if cache { if err := s.messageCache.AddMessage(m); err != nil { @@ -467,14 +479,16 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) sendToFirebase(v *visitor, m *message) { + log.Debug("%s Publishing to Firebase", logPrefix(v, m)) if err := s.firebaseClient.Send(v, m); err != nil { - log.Warn("[%s] FB - Unable to publish to Firebase: %v", v.ip, err.Error()) + log.Warn("%s Unable to publish to Firebase: %v", logPrefix(v, m), err.Error()) } } func (s *Server) sendEmail(v *visitor, m *message, email string) { + log.Debug("%s Sending email to %s", logPrefix(v, m), email) if err := s.mailer.Send(v.ip, email, m); err != nil { - log.Warn("[%s] MAIL - Unable to send email: %v", v.ip, err.Error()) + log.Warn("%s Unable to send email: %v", logPrefix(v, m), err.Error()) } } @@ -482,9 +496,10 @@ 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) + log.Debug("%s Publishing poll request to %s", logPrefix(v, m), forwardURL) req, err := http.NewRequest("POST", forwardURL, strings.NewReader("")) if err != nil { - log.Warn("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) + log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) return } req.Header.Set("X-Poll-ID", m.ID) @@ -493,10 +508,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } response, err := httpClient.Do(req) if err != nil { - log.Warn("[%s] FWD - Unable to forward poll request: %v", v.ip, err.Error()) + log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) return } else if response.StatusCode != http.StatusOK { - log.Warn("[%s] FWD - Unable to forward poll request, unexpected status: %d", v.ip, response.StatusCode) + log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logPrefix(v, m), response.StatusCode) return } } @@ -1012,6 +1027,7 @@ func (s *Server) updateStatsAndPrune() { // Expire visitors from rate visitors map for ip, v := range s.visitors { if v.Stale() { + log.Debug("Deleting stale visitor %s", v.ip) delete(s.visitors, ip) } } @@ -1019,17 +1035,21 @@ func (s *Server) updateStatsAndPrune() { // Delete expired attachments if s.fileCache != nil { ids, err := s.messageCache.AttachmentsExpired() - if err == nil { + if err != nil { + log.Warn("Error retrieving expired attachments: %s", err.Error()) + } else if len(ids) > 0 { + log.Debug("Deleting expired attachments: %v", ids) if err := s.fileCache.Remove(ids...); err != nil { log.Warn("Error deleting attachments: %s", err.Error()) } } else { - log.Warn("Error retrieving expired attachments: %s", err.Error()) + log.Debug("No expired attachments to delete") } } // Prune message cache olderThan := time.Now().Add(-1 * s.config.CacheDuration) + log.Debug("Pruning messages older tha %v", olderThan) if err := s.messageCache.Prune(olderThan); err != nil { log.Warn("Error pruning cache: %s", err.Error()) } @@ -1079,6 +1099,7 @@ func (s *Server) runManager() { for { select { case <-time.After(s.config.ManagerInterval): + log.Debug("Running manager") s.updateStatsAndPrune() case <-s.closeChan: return @@ -1124,7 +1145,7 @@ func (s *Server) sendDelayedMessages() error { for _, m := range messages { v := s.visitorFromIP(m.Sender) if err := s.sendDelayedMessage(v, m); err != nil { - log.Warn("error sending delayed message: %s", err.Error()) + log.Warn("%s Error sending delayed message: %s", logPrefix(v, m), err.Error()) } } return nil @@ -1133,12 +1154,13 @@ func (s *Server) sendDelayedMessages() error { func (s *Server) sendDelayedMessage(v *visitor, m *message) error { s.mu.Lock() defer s.mu.Unlock() + log.Debug("%s Sending delayed message", logPrefix(v, m)) t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published if ok { go func() { // We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler if err := t.Publish(v, m); err != nil { - log.Warn("unable to publish message %s to topic %s: %v", m.ID, m.Topic, err.Error()) + log.Warn("%s Unable to publish message: %v", logPrefix(v, m), err.Error()) } }() } @@ -1311,3 +1333,7 @@ func (s *Server) visitorFromIP(ip string) *visitor { v.Keepalive() return v } + +func logPrefix(v *visitor, m *message) string { + return fmt.Sprintf("%s/%s/%s", v.ip, m.Topic, m.ID) +} diff --git a/server/server.yml b/server/server.yml index ce7b1c75..f6c14a64 100644 --- a/server/server.yml +++ b/server/server.yml @@ -178,3 +178,8 @@ # # visitor-attachment-total-size-limit: "100M" # visitor-attachment-daily-bandwidth-limit: "500M" + +# Log level, can be DEBUG, INFO, WARN or ERROR +# This option can be hot-reloaded by calling "kill -HUP $pid" or "systemctl reload ntfy". +# +# log-level: INFO diff --git a/server/topic.go b/server/topic.go index eb53225b..3cb11394 100644 --- a/server/topic.go +++ b/server/topic.go @@ -1,7 +1,7 @@ package server import ( - "log" + "heckel.io/ntfy/log" "math/rand" "sync" ) @@ -46,10 +46,15 @@ func (t *topic) Publish(v *visitor, m *message) error { go func() { t.mu.Lock() defer t.mu.Unlock() - for _, s := range t.subscribers { - if err := s(v, m); err != nil { - log.Printf("error publishing message to subscriber") + if len(t.subscribers) > 0 { + log.Debug("%s Forwarding to %d subscriber(s)", logPrefix(v, m), len(t.subscribers)) + for _, s := range t.subscribers { + if err := s(v, m); err != nil { + log.Warn("%s Error forwarding to subscriber", logPrefix(v, m)) + } } + } else { + log.Debug("%s No subscribers, not forwarding", logPrefix(v, m)) } }() return nil From 430f985fca098bdf364e74d118f864f9c06cfbf4 Mon Sep 17 00:00:00 2001 From: ksurl Date: Wed, 1 Jun 2022 16:49:08 -0700 Subject: [PATCH 21/38] update docker docs --- docker-compose.yml | 14 ++++++++++++++ docs/install.md | 6 ++++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..365f25da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "2.1" +services: + ntfy: + image: binwiederhier/ntfy + container_name: ntfy + command: + - serve + volumes: + - /var/cache/ntfy:/var/cache/ntfy + - /etc/ntfy:/etc/ntfy + ports: + - 80:80 + restart: unless-stopped + diff --git a/docs/install.md b/docs/install.md index 231b4306..ed741231 100644 --- a/docs/install.md +++ b/docs/install.md @@ -239,17 +239,18 @@ docker run \ serve ``` -With other config options (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): +With other config options and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): ```bash docker run \ -v /etc/ntfy:/etc/ntfy \ -p 80:80 \ + -u UID:GID \ -it \ binwiederhier/ntfy \ serve ``` -Using docker-compose: +Using docker-compose with non-root user: ```yaml version: "2.1" @@ -259,6 +260,7 @@ services: container_name: ntfy command: - serve + user: UID:GID # optional. replace with your own user/group or uid/gid volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy From a898a2ebe832091ba489fe63c0a1d43e10d55297 Mon Sep 17 00:00:00 2001 From: ksurl Date: Wed, 1 Jun 2022 16:50:42 -0700 Subject: [PATCH 22/38] add user to compose file --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 365f25da..d4841bc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,7 @@ services: container_name: ntfy command: - serve + user: UID:GID # optional. Set custom user/group or uid/gid volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy From 732537eabae3e3a4124316ba70fd76283177bbf4 Mon Sep 17 00:00:00 2001 From: ksurl Date: Wed, 1 Jun 2022 16:52:37 -0700 Subject: [PATCH 23/38] add chown warning --- docs/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/install.md b/docs/install.md index ed741231..595bbd9f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -269,6 +269,8 @@ services: restart: unless-stopped ``` +If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files to the same uid/gid. + Alternatively, you may wish to build a customized Docker image that can be run with fewer command-line arguments and without delivering the configuration file separately. ``` FROM binwiederhier/ntfy From 7845eb01243d5866146101c78363c58f510e72e3 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Jun 2022 23:24:44 -0400 Subject: [PATCH 24/38] So much logging --- docs/privacy.md | 4 +- docs/releases.md | 5 ++ log/log.go | 39 +++++++-- server/server.go | 170 +++++++++++++++++++++----------------- server/server.yml | 6 +- server/server_firebase.go | 12 +-- server/server_test.go | 16 ++-- server/smtp_sender.go | 51 +++++++++--- server/smtp_server.go | 45 +++++++--- server/topic.go | 6 +- server/util.go | 30 +++++++ web/public/home.html | 2 +- 12 files changed, 264 insertions(+), 122 deletions(-) diff --git a/docs/privacy.md b/docs/privacy.md index 5a36a1c8..f89f9aaa 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -8,5 +8,5 @@ any outside service. All data is exclusively used to make the service function p I use is Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see [FAQ](faq.md) for details). To avoid FCM altogether, download the F-Droid version. -The web server does not log or otherwise store request paths, remote IP addresses or even topics or messages, -aside from a short on-disk cache to support service restarts. +For debugging purposes, the ntfy server may temporarily log request paths, remote IP addresses or even topics +or messages, though typically this is turned off. diff --git a/docs/releases.md b/docs/releases.md index 1a3fa4d6..a8d3fe10 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -13,6 +13,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## ntfy server v1.25.0 (UNRELEASED) +**Features:** + +* Advanced logging, with different log levels and hot reloading of the log level (no ticket) + **Bugs**: * Respect Firebase "quota exceeded" response for topics, block Firebase publishing for user for 10min ([#289](https://github.com/binwiederhier/ntfy/issues/289)) @@ -27,6 +31,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) * Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) * Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting) +* Example for using ntfy with docker-compose.yml without root privileges ([#304](https://github.com/binwiederhier/ntfy/pull/304), thanks to [@ksurl](https://github.com/ksurl)) **Additional translations:** diff --git a/log/log.go b/log/log.go index 36abc0e3..f105f720 100644 --- a/log/log.go +++ b/log/log.go @@ -11,7 +11,8 @@ type Level int // Well known log levels const ( - DebugLevel Level = iota + TraceLevel Level = iota + DebugLevel InfoLevel WarnLevel ErrorLevel @@ -19,6 +20,8 @@ const ( func (l Level) String() string { switch l { + case TraceLevel: + return "TRACE" case DebugLevel: return "DEBUG" case InfoLevel: @@ -36,7 +39,12 @@ var ( mu = &sync.Mutex{} ) -// Debug prints the given message, if the current log level is DEBUG +// Trace prints the given message, if the current log level is TRACE +func Trace(message string, v ...interface{}) { + logIf(TraceLevel, message, v...) +} + +// Debug prints the given message, if the current log level is DEBUG or lower func Debug(message string, v ...interface{}) { logIf(DebugLevel, message, v...) } @@ -78,20 +86,37 @@ func SetLevel(newLevel Level) { // ToLevel converts a string to a Level. It returns InfoLevel if the string // does not match any known log levels. func ToLevel(s string) Level { - switch strings.ToLower(s) { - case "debug": + switch strings.ToUpper(s) { + case "TRACE": + return TraceLevel + case "DEBUG": return DebugLevel - case "info": + case "INFO": return InfoLevel - case "warn", "warning": + case "WARN", "WARNING": return WarnLevel - case "error": + case "ERROR": return ErrorLevel default: return InfoLevel } } +// Loggable returns true if the given log level is lower or equal to the current log level +func Loggable(l Level) bool { + return CurrentLevel() <= l +} + +// IsTrace returns true if the current log level is TraceLevel +func IsTrace() bool { + return Loggable(TraceLevel) +} + +// IsDebug returns true if the current log level is DebugLevel or below +func IsDebug() bool { + return Loggable(DebugLevel) +} + func logIf(l Level, message string, v ...interface{}) { if CurrentLevel() <= l { log.Printf(l.String()+" "+message, v...) diff --git a/server/server.go b/server/server.go index 4688a03f..98299213 100644 --- a/server/server.go +++ b/server/server.go @@ -32,22 +32,22 @@ import ( // Server is the main server, providing the UI and API for ntfy type Server struct { - config *Config - httpServer *http.Server - httpsServer *http.Server - unixListener net.Listener - smtpServer *smtp.Server - smtpBackend *smtpBackend - topics map[string]*topic - visitors map[string]*visitor - firebaseClient *firebaseClient - mailer mailer - messages int64 - auth auth.Auther - messageCache *messageCache - fileCache *fileCache - closeChan chan bool - mu sync.Mutex + config *Config + httpServer *http.Server + httpsServer *http.Server + unixListener net.Listener + smtpServer *smtp.Server + smtpServerBackend *smtpBackend + smtpSender mailer + topics map[string]*topic + visitors map[string]*visitor + firebaseClient *firebaseClient + messages int64 + auth auth.Auther + messageCache *messageCache + fileCache *fileCache + closeChan chan bool + mu sync.Mutex } // handleFunc extends the normal http.HandlerFunc to be able to easily return errors @@ -147,7 +147,7 @@ func New(conf *Config) (*Server, error) { messageCache: messageCache, fileCache: fileCache, firebaseClient: firebaseClient, - mailer: mailer, + smtpSender: mailer, topics: topics, auth: auther, visitors: make(map[string]*visitor), @@ -246,14 +246,14 @@ func (s *Server) Stop() { func (s *Server) handle(w http.ResponseWriter, r *http.Request) { v := s.visitor(r) - log.Debug("%s HTTP %s %s", v.ip, r.Method, r.URL.Path) + log.Debug("%s Dispatching request", logHTTPPrefix(v, r)) if err := s.handleInternal(w, r, v); err != nil { if websocket.IsWebSocketUpgrade(r) { - isNormalError := websocket.IsCloseError(err, websocket.CloseAbnormalClosure) || strings.Contains(err.Error(), "i/o timeout") + isNormalError := strings.Contains(err.Error(), "i/o timeout") if isNormalError { - log.Debug("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + log.Debug("%s WebSocket error (this error is okay, it happens a lot): %s", logHTTPPrefix(v, r), err.Error()) } else { - log.Warn("%s WS %s %s - %s", v.ip, r.Method, r.URL.Path, err.Error()) + log.Warn("%s WebSocket error: %s", logHTTPPrefix(v, r), err.Error()) } return // Do not attempt to write to upgraded connection } @@ -261,13 +261,12 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) { if !ok { httpErr = errHTTPInternalError } - isNormalError := httpErr.Code == 404 + isNormalError := httpErr.HTTPCode == http.StatusNotFound if isNormalError { - log.Debug("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + log.Debug("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error()) } else { - log.Info("%s HTTP %s %s - %d - %d - %s", v.ip, r.Method, r.URL.Path, httpErr.HTTPCode, httpErr.Code, err.Error()) + log.Info("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error()) } - w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests w.WriteHeader(httpErr.HTTPCode) @@ -444,8 +443,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito m.Message = emptyMessageBody } delayed := m.Time > time.Now().Unix() - log.Debug("%s Received message: ev=%s, body=%d bytes, delayed=%t, fb=%t, cache=%t, up=%t, email=%s", - logPrefix(v, m), m.Event, len(body.PeekedBytes), delayed, firebase, cache, unifiedpush, email) + log.Debug("%s Received message: event=%s, body=%d byte(s), delayed=%t, firebase=%t, cache=%t, up=%t, email=%s", + logMessagePrefix(v, m), m.Event, len(m.Message), delayed, firebase, cache, unifiedpush, email) + if log.IsTrace() { + log.Trace("%s Message body: %s", logMessagePrefix(v, m), maybeMarshalJSON(m)) + } if !delayed { if err := t.Publish(v, m); err != nil { return err @@ -453,14 +455,14 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito if s.firebaseClient != nil && firebase { go s.sendToFirebase(v, m) } - if s.mailer != nil && email != "" { + if s.smtpSender != nil && email != "" { go s.sendEmail(v, m, email) } if s.config.UpstreamBaseURL != "" { go s.forwardPollRequest(v, m) } } else { - log.Debug("%s Message delayed, will process later", logPrefix(v, m)) + log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m)) } if cache { if err := s.messageCache.AddMessage(m); err != nil { @@ -479,16 +481,16 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } func (s *Server) sendToFirebase(v *visitor, m *message) { - log.Debug("%s Publishing to Firebase", logPrefix(v, m)) + log.Debug("%s Publishing to Firebase", logMessagePrefix(v, m)) if err := s.firebaseClient.Send(v, m); err != nil { - log.Warn("%s Unable to publish to Firebase: %v", logPrefix(v, m), err.Error()) + log.Warn("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) } } func (s *Server) sendEmail(v *visitor, m *message, email string) { - log.Debug("%s Sending email to %s", logPrefix(v, m), email) - if err := s.mailer.Send(v.ip, email, m); err != nil { - log.Warn("%s Unable to send email: %v", logPrefix(v, m), err.Error()) + log.Debug("%s Sending email to %s", logMessagePrefix(v, m), email) + if err := s.smtpSender.Send(v, m, email); err != nil { + log.Warn("%s Unable to send email: %v", logMessagePrefix(v, m), err.Error()) } } @@ -496,10 +498,10 @@ 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) - log.Debug("%s Publishing poll request to %s", logPrefix(v, m), forwardURL) + log.Debug("%s Publishing poll request to %s", logMessagePrefix(v, m), forwardURL) req, err := http.NewRequest("POST", forwardURL, strings.NewReader("")) if err != nil { - log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) + log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error()) return } req.Header.Set("X-Poll-ID", m.ID) @@ -508,10 +510,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } response, err := httpClient.Do(req) if err != nil { - log.Warn("%s Unable to publish poll request: %v", logPrefix(v, m), err.Error()) + log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error()) return } else if response.StatusCode != http.StatusOK { - log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logPrefix(v, m), response.StatusCode) + log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logMessagePrefix(v, m), response.StatusCode) return } } @@ -553,7 +555,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca return false, false, "", false, errHTTPTooManyRequestsLimitEmails } } - if s.mailer == nil && email != "" { + if s.smtpSender == nil && email != "" { return false, false, "", false, errHTTPBadRequestEmailDisabled } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") @@ -627,7 +629,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca // If file.txt is > message limit, treat it as an attachment func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 - return nil + return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { @@ -640,6 +642,12 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body return s.handleBodyAsAttachment(r, v, m, body) // Case 6 } +func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error { + _, err := io.Copy(io.Discard, body) + _ = body.Close() + return err +} + func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedReadCloser) error { if utf8.Valid(body.PeekedBytes) { m.Message = string(body.PeekedBytes) // Do not trim @@ -739,6 +747,8 @@ func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *v } func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error { + log.Debug("%s HTTP stream connection opened", logHTTPPrefix(v, r)) + defer log.Debug("%s HTTP stream connection closed", logHTTPPrefix(v, r)) if err := v.SubscriptionAllowed(); err != nil { return errHTTPTooManyRequestsLimitSubscriptions } @@ -795,6 +805,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * case <-r.Context().Done(): return nil case <-time.After(s.config.KeepaliveInterval): + log.Trace("%s Sending keepalive message", logHTTPPrefix(v, r)) v.Keepalive() if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message return err @@ -811,6 +822,8 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi return errHTTPTooManyRequestsLimitSubscriptions } defer v.RemoveSubscription() + log.Debug("%s WebSocket connection opened", logHTTPPrefix(v, r)) + defer log.Debug("%s WebSocket connection closed", logHTTPPrefix(v, r)) topics, topicsStr, err := s.topicsFromPath(r.URL.Path) if err != nil { return err @@ -840,6 +853,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi return err } conn.SetPongHandler(func(appData string) error { + log.Trace("%s Received WebSocket pong", logHTTPPrefix(v, r)) return conn.SetReadDeadline(time.Now().Add(pongWait)) }) for { @@ -856,6 +870,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi if err := conn.SetWriteDeadline(time.Now().Add(wsWriteWait)); err != nil { return err } + log.Trace("%s Sending WebSocket ping", logHTTPPrefix(v, r)) return conn.WriteMessage(websocket.PingMessage, nil) } for { @@ -901,8 +916,9 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi return err } err = g.Wait() - if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { - return nil // Normal closures are not errors + if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Trace("%s WebSocket connection closed: %s", logHTTPPrefix(v, r), err.Error()) + return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot } return err } @@ -1025,12 +1041,15 @@ func (s *Server) updateStatsAndPrune() { defer s.mu.Unlock() // Expire visitors from rate visitors map + staleVisitors := 0 for ip, v := range s.visitors { if v.Stale() { log.Debug("Deleting stale visitor %s", v.ip) delete(s.visitors, ip) + staleVisitors++ } } + log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors) // Delete expired attachments if s.fileCache != nil { @@ -1038,20 +1057,20 @@ func (s *Server) updateStatsAndPrune() { if err != nil { log.Warn("Error retrieving expired attachments: %s", err.Error()) } else if len(ids) > 0 { - log.Debug("Deleting expired attachments: %v", ids) + log.Debug("Manager: Deleting expired attachments: %v", ids) if err := s.fileCache.Remove(ids...); err != nil { log.Warn("Error deleting attachments: %s", err.Error()) } } else { - log.Debug("No expired attachments to delete") + log.Debug("Manager: No expired attachments to delete") } } // Prune message cache olderThan := time.Now().Add(-1 * s.config.CacheDuration) - log.Debug("Pruning messages older tha %v", olderThan) + log.Debug("Manager: Pruning messages older than %s", olderThan.Format("2006-01-02 15:04:05")) if err := s.messageCache.Prune(olderThan); err != nil { - log.Warn("Error pruning cache: %s", err.Error()) + log.Warn("Manager: Error pruning cache: %s", err.Error()) } // Prune old topics, remove subscriptions without subscribers @@ -1060,7 +1079,7 @@ func (s *Server) updateStatsAndPrune() { subs := t.Subscribers() msgs, err := s.messageCache.MessageCount(t.ID) if err != nil { - log.Warn("Cannot get stats for topic %s: %s", t.ID, err.Error()) + log.Warn("Manager: Cannot get stats for topic %s: %s", t.ID, err.Error()) continue } if msgs == 0 && subs == 0 { @@ -1072,19 +1091,25 @@ func (s *Server) updateStatsAndPrune() { } // Mail stats - var mailSuccess, mailFailure int64 - if s.smtpBackend != nil { - mailSuccess, mailFailure = s.smtpBackend.Counts() + var receivedMailTotal, receivedMailSuccess, receivedMailFailure int64 + if s.smtpServerBackend != nil { + receivedMailTotal, receivedMailSuccess, receivedMailFailure = s.smtpServerBackend.Counts() + } + var sentMailTotal, sentMailSuccess, sentMailFailure int64 + if s.smtpSender != nil { + sentMailTotal, sentMailSuccess, sentMailFailure = s.smtpSender.Counts() } // Print stats - log.Info("Stats: %d message(s) published, %d in cache, %d successful mails, %d failed, %d topic(s) active, %d subscriber(s), %d visitor(s)", - s.messages, messages, mailSuccess, mailFailure, len(s.topics), subscribers, len(s.visitors)) + log.Info("Stats: %d messages published, %d in cache, %d topic(s) active, %d subscriber(s), %d visitor(s), %d mails received (%d successful, %d failed), %d mails sent (%d successful, %d failed)", + s.messages, messages, len(s.topics), subscribers, len(s.visitors), + receivedMailTotal, receivedMailSuccess, receivedMailFailure, + sentMailTotal, sentMailSuccess, sentMailFailure) } func (s *Server) runSMTPServer() error { - s.smtpBackend = newMailBackend(s.config, s.handle) - s.smtpServer = smtp.NewServer(s.smtpBackend) + s.smtpServerBackend = newMailBackend(s.config, s.handle) + s.smtpServer = smtp.NewServer(s.smtpServerBackend) s.smtpServer.Addr = s.config.SMTPServerListen s.smtpServer.Domain = s.config.SMTPServerDomain s.smtpServer.ReadTimeout = 10 * time.Second @@ -1099,7 +1124,6 @@ func (s *Server) runManager() { for { select { case <-time.After(s.config.ManagerInterval): - log.Debug("Running manager") s.updateStatsAndPrune() case <-s.closeChan: return @@ -1107,19 +1131,6 @@ func (s *Server) runManager() { } } -func (s *Server) runDelayedSender() { - for { - select { - case <-time.After(s.config.DelayedSenderInterval): - if err := s.sendDelayedMessages(); err != nil { - log.Warn("error sending scheduled messages: %s", err.Error()) - } - case <-s.closeChan: - return - } - } -} - func (s *Server) runFirebaseKeepaliver() { if s.firebaseClient == nil { return @@ -1137,6 +1148,19 @@ func (s *Server) runFirebaseKeepaliver() { } } +func (s *Server) runDelayedSender() { + for { + select { + case <-time.After(s.config.DelayedSenderInterval): + if err := s.sendDelayedMessages(); err != nil { + log.Warn("Error sending delayed messages: %s", err.Error()) + } + case <-s.closeChan: + return + } + } +} + func (s *Server) sendDelayedMessages() error { messages, err := s.messageCache.MessagesDue() if err != nil { @@ -1145,7 +1169,7 @@ func (s *Server) sendDelayedMessages() error { for _, m := range messages { v := s.visitorFromIP(m.Sender) if err := s.sendDelayedMessage(v, m); err != nil { - log.Warn("%s Error sending delayed message: %s", logPrefix(v, m), err.Error()) + log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error()) } } return nil @@ -1154,13 +1178,13 @@ func (s *Server) sendDelayedMessages() error { func (s *Server) sendDelayedMessage(v *visitor, m *message) error { s.mu.Lock() defer s.mu.Unlock() - log.Debug("%s Sending delayed message", logPrefix(v, m)) + log.Debug("%s Sending delayed message", logMessagePrefix(v, m)) t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published if ok { go func() { // We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler if err := t.Publish(v, m); err != nil { - log.Warn("%s Unable to publish message: %v", logPrefix(v, m), err.Error()) + log.Warn("%s Unable to publish message: %v", logMessagePrefix(v, m), err.Error()) } }() } @@ -1333,7 +1357,3 @@ func (s *Server) visitorFromIP(ip string) *visitor { v.Keepalive() return v } - -func logPrefix(v *visitor, m *message) string { - return fmt.Sprintf("%s/%s/%s", v.ip, m.Topic, m.ID) -} diff --git a/server/server.yml b/server/server.yml index f6c14a64..2d62b704 100644 --- a/server/server.yml +++ b/server/server.yml @@ -51,6 +51,7 @@ # cache-file: # cache-duration: "12h" + # If set, access to the ntfy server and API can be controlled on a granular level using # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs. # @@ -179,7 +180,10 @@ # visitor-attachment-total-size-limit: "100M" # visitor-attachment-daily-bandwidth-limit: "500M" -# Log level, can be DEBUG, INFO, WARN or ERROR +# Log level, can be TRACE, DEBUG, INFO, WARN or ERROR # This option can be hot-reloaded by calling "kill -HUP $pid" or "systemctl reload ntfy". # +# Be aware that DEBUG (and particularly TRACE) can be VERY CHATTY. Only turn them on for +# debugging purposes, or your disk will fill up quickly. +# # log-level: INFO diff --git a/server/server_firebase.go b/server/server_firebase.go index b34348d6..2abc10de 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -4,14 +4,13 @@ import ( "context" "encoding/json" "errors" - "fmt" - "log" - "strings" - firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" + "fmt" "google.golang.org/api/option" "heckel.io/ntfy/auth" + "heckel.io/ntfy/log" + "strings" ) const ( @@ -45,9 +44,12 @@ func (c *firebaseClient) Send(v *visitor, m *message) error { if err != nil { return err } + if log.IsTrace() { + log.Trace("%s Firebase message: %s", logMessagePrefix(v, m), maybeMarshalJSON(fbm)) + } err = c.sender.Send(fbm) if err == errFirebaseQuotaExceeded { - log.Printf("[%s] FB quota exceeded for topic %s, temporarily denying FB access to visitor", v.ip, m.Topic) + log.Warn("%s Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor", logMessagePrefix(v, m)) v.FirebaseTemporarilyDeny() } return err diff --git a/server/server_test.go b/server/server_test.go index ce63f272..8010643f 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -477,7 +477,7 @@ func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) { func TestServer_PublishInvalidTopic(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} response := request(t, s, "PUT", "/docs", "fail", nil) require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code) } @@ -743,13 +743,17 @@ type testMailer struct { mu sync.Mutex } -func (t *testMailer) Send(from, to string, m *message) error { +func (t *testMailer) Send(v *visitor, m *message, to string) error { t.mu.Lock() defer t.mu.Unlock() t.count++ return nil } +func (t *testMailer) Counts() (total int64, success int64, failure int64) { + return 0, 0, 0 +} + func (t *testMailer) Count() int { t.mu.Lock() defer t.mu.Unlock() @@ -795,7 +799,7 @@ func TestServer_PublishTooRequests_ShortReplenish(t *testing.T) { func TestServer_PublishTooManyEmails_Defaults(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} for i := 0; i < 16; i++ { response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ "E-Mail": "test@example.com", @@ -812,7 +816,7 @@ func TestServer_PublishTooManyEmails_Replenish(t *testing.T) { c := newTestConfig(t) c.VisitorEmailLimitReplenish = 500 * time.Millisecond s := newTestServer(t, c) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} for i := 0; i < 16; i++ { response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ "E-Mail": "test@example.com", @@ -838,7 +842,7 @@ func TestServer_PublishTooManyEmails_Replenish(t *testing.T) { func TestServer_PublishDelayedEmail_Fail(t *testing.T) { s := newTestServer(t, newTestConfig(t)) - s.mailer = &testMailer{} + s.smtpSender = &testMailer{} response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ "E-Mail": "test@example.com", "Delay": "20 min", @@ -956,7 +960,7 @@ func TestServer_PublishAsJSON(t *testing.T) { func TestServer_PublishAsJSON_WithEmail(t *testing.T) { mailer := &testMailer{} s := newTestServer(t, newTestConfig(t)) - s.mailer = mailer + s.smtpSender = mailer body := `{"topic":"mytopic","message":"A message","email":"phil@example.com"}` response := request(t, s, "PUT", "/", body, nil) require.Equal(t, 200, response.Code) diff --git a/server/smtp_sender.go b/server/smtp_sender.go index 15f004c1..1ccaf084 100644 --- a/server/smtp_sender.go +++ b/server/smtp_sender.go @@ -4,33 +4,62 @@ import ( _ "embed" // required by go:embed "encoding/json" "fmt" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" "mime" "net" "net/smtp" "strings" + "sync" "time" ) type mailer interface { - Send(from, to string, m *message) error + Send(v *visitor, m *message, to string) error + Counts() (total int64, success int64, failure int64) } type smtpSender struct { - config *Config + config *Config + success int64 + failure int64 + mu sync.Mutex } -func (s *smtpSender) Send(senderIP, to string, m *message) error { - host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr) +func (s *smtpSender) Send(v *visitor, m *message, to string) error { + return s.withCount(v, m, func() error { + host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr) + if err != nil { + return err + } + message, err := formatMail(s.config.BaseURL, v.ip, s.config.SMTPSenderFrom, to, m) + if err != nil { + return err + } + auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host) + log.Debug("%s Sending mail: via=%s, user=%s, pass=***, to=%s", logMessagePrefix(v, m), s.config.SMTPSenderAddr, s.config.SMTPSenderUser, to) + log.Trace("%s Mail body: %s", logMessagePrefix(v, m), message) + return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message)) + }) +} + +func (s *smtpSender) Counts() (total int64, success int64, failure int64) { + s.mu.Lock() + defer s.mu.Unlock() + return s.success + s.failure, s.success, s.failure +} + +func (s *smtpSender) withCount(v *visitor, m *message, fn func() error) error { + err := fn() + s.mu.Lock() + defer s.mu.Unlock() if err != nil { - return err + log.Debug("%s Sending mail failed: %s", logMessagePrefix(v, m), err.Error()) + s.failure++ + } else { + s.success++ } - message, err := formatMail(s.config.BaseURL, senderIP, s.config.SMTPSenderFrom, to, m) - if err != nil { - return err - } - auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host) - return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message)) + return err } func formatMail(baseURL, senderIP, from, to string, m *message) (string, error) { diff --git a/server/smtp_server.go b/server/smtp_server.go index 7812371e..3f4b9b68 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "github.com/emersion/go-smtp" + "heckel.io/ntfy/log" "io" "mime" "mime/multipart" + "net" "net/http" "net/http/httptest" "net/mail" @@ -40,36 +42,41 @@ func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Reques } func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { - return &smtpSession{backend: b, remoteAddr: state.RemoteAddr.String()}, nil + log.Debug("%s Incoming mail, login with user %s", logSMTPPrefix(state), username) + return &smtpSession{backend: b, state: state}, nil } func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { - return &smtpSession{backend: b, remoteAddr: state.RemoteAddr.String()}, nil + log.Debug("%s Incoming mail, anonymous login", logSMTPPrefix(state)) + return &smtpSession{backend: b, state: state}, nil } -func (b *smtpBackend) Counts() (success int64, failure int64) { +func (b *smtpBackend) Counts() (total int64, success int64, failure int64) { b.mu.Lock() defer b.mu.Unlock() - return b.success, b.failure + return b.success + b.failure, b.success, b.failure } // smtpSession is returned after EHLO. type smtpSession struct { - backend *smtpBackend - remoteAddr string - topic string - mu sync.Mutex + backend *smtpBackend + state *smtp.ConnectionState + topic string + mu sync.Mutex } func (s *smtpSession) AuthPlain(username, password string) error { + log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.state), username) return nil } func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error { + log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.state), from, opts) return nil } func (s *smtpSession) Rcpt(to string) error { + log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.state), to) return s.withFailCount(func() error { conf := s.backend.config addressList, err := mail.ParseAddressList(to) @@ -106,6 +113,11 @@ func (s *smtpSession) Data(r io.Reader) error { if err != nil { return err } + if log.IsTrace() { + log.Trace("%s DATA: %s", logSMTPPrefix(s.state), string(b)) + } else if log.IsDebug() { + log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.state), len(b)) + } msg, err := mail.ReadMessage(bytes.NewReader(b)) if err != nil { return err @@ -143,10 +155,18 @@ func (s *smtpSession) Data(r io.Reader) error { } func (s *smtpSession) publishMessage(m *message) error { + // Extract remote address (for rate limiting) + remoteAddr, _, err := net.SplitHostPort(s.state.RemoteAddr.String()) + if err != nil { + remoteAddr = s.state.RemoteAddr.String() + } + + // Call HTTP handler with fake HTTP request url := fmt.Sprintf("%s/%s", s.backend.config.BaseURL, m.Topic) - req, err := http.NewRequest("PUT", url, strings.NewReader(m.Message)) - req.RemoteAddr = s.remoteAddr // rate limiting!! - req.Header.Set("X-Forwarded-For", s.remoteAddr) + req, err := http.NewRequest("POST", url, strings.NewReader(m.Message)) + req.RequestURI = "/" + m.Topic // just for the logs + req.RemoteAddr = remoteAddr // rate limiting!! + req.Header.Set("X-Forwarded-For", remoteAddr) if err != nil { return err } @@ -176,6 +196,9 @@ func (s *smtpSession) withFailCount(fn func() error) error { s.backend.mu.Lock() defer s.backend.mu.Unlock() if err != nil { + // Almost all of these errors are parse errors, and user input errors. + // We do not want to spam the log with WARN messages. + log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.state), err.Error()) s.backend.failure++ } return err diff --git a/server/topic.go b/server/topic.go index 3cb11394..889f1eb7 100644 --- a/server/topic.go +++ b/server/topic.go @@ -47,14 +47,14 @@ func (t *topic) Publish(v *visitor, m *message) error { t.mu.Lock() defer t.mu.Unlock() if len(t.subscribers) > 0 { - log.Debug("%s Forwarding to %d subscriber(s)", logPrefix(v, m), len(t.subscribers)) + log.Debug("%s Forwarding to %d subscriber(s)", logMessagePrefix(v, m), len(t.subscribers)) for _, s := range t.subscribers { if err := s(v, m); err != nil { - log.Warn("%s Error forwarding to subscriber", logPrefix(v, m)) + log.Warn("%s Error forwarding to subscriber", logMessagePrefix(v, m)) } } } else { - log.Debug("%s No subscribers, not forwarding", logPrefix(v, m)) + log.Trace("%s No stream or WebSocket subscribers, not forwarding", logMessagePrefix(v, m)) } }() return nil diff --git a/server/util.go b/server/util.go index 7c596344..ffd07d15 100644 --- a/server/util.go +++ b/server/util.go @@ -1,6 +1,9 @@ package server import ( + "encoding/json" + "fmt" + "github.com/emersion/go-smtp" "net/http" "strings" ) @@ -40,3 +43,30 @@ func readQueryParam(r *http.Request, names ...string) string { } return "" } + +func logMessagePrefix(v *visitor, m *message) string { + return fmt.Sprintf("%s/%s/%s", v.ip, m.Topic, m.ID) +} + +func logHTTPPrefix(v *visitor, r *http.Request) string { + requestURI := r.RequestURI + if requestURI == "" { + requestURI = r.URL.Path + } + return fmt.Sprintf("%s HTTP %s %s", v.ip, r.Method, requestURI) +} + +func logSMTPPrefix(state *smtp.ConnectionState) string { + return fmt.Sprintf("%s/%s SMTP", state.Hostname, state.RemoteAddr.String()) +} + +func maybeMarshalJSON(v interface{}) string { + messageJSON, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "" + } + if len(messageJSON) > 5000 { + return string(messageJSON)[:5000] + } + return string(messageJSON) +} diff --git a/web/public/home.html b/web/public/home.html index 3e3a95d8..43007ca3 100644 --- a/web/public/home.html +++ b/web/public/home.html @@ -110,7 +110,7 @@

- +

Here's a video showing the app in action: From 5cc0b194d3676ce54c9bbb9af407ff9c4f61d6f4 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 10:50:05 -0400 Subject: [PATCH 25/38] Add --trace and --no-log-dates; add docs --- cmd/app.go | 9 +- cmd/subscribe_windows.go | 2 +- docs/config.md | 168 ++++++++++++++--------- go.mod | 10 +- go.sum | 18 +-- log/log.go | 5 + server/ntfy.service | 2 +- server/server.go | 2 +- web/package-lock.json | 287 ++++++++++++++++++++------------------- 9 files changed, 286 insertions(+), 217 deletions(-) diff --git a/cmd/app.go b/cmd/app.go index adac9d73..975fd2fa 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -17,6 +17,8 @@ var commands = make([]*cli.Command, 0) var flagsDefault = []cli.Flag{ &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"NTFY_DEBUG"}, Usage: "enable debug logging"}, + &cli.BoolFlag{Name: "trace", EnvVars: []string{"NTFY_TRACE"}, Usage: "enable tracing (very verbose, be careful)"}, + &cli.BoolFlag{Name: "no-log-dates", Aliases: []string{"no_log_dates"}, EnvVars: []string{"NTFY_NO_LOG_DATES"}, Usage: "disable the date/time prefix"}, altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}), } @@ -38,10 +40,15 @@ func New() *cli.App { } func initLogFunc(c *cli.Context) error { - if c.Bool("debug") { + if c.Bool("trace") { + log.SetLevel(log.TraceLevel) + } else if c.Bool("debug") { log.SetLevel(log.DebugLevel) } else { log.SetLevel(log.ToLevel(c.String("log-level"))) } + if c.Bool("no-log-dates") { + log.DisableDates() + } return nil } diff --git a/cmd/subscribe_windows.go b/cmd/subscribe_windows.go index 129e8f52..e8f1a271 100644 --- a/cmd/subscribe_windows.go +++ b/cmd/subscribe_windows.go @@ -10,6 +10,6 @@ var ( scriptLauncher = []string{"cmd.exe", "/Q", "/C"} ) -func defaultConfigFile() string { +func defaultClientConfigFile() string { return defaultClientConfigFileWindows() } diff --git a/docs/config.md b/docs/config.md index 9b0aa07c..fc1c115e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -708,6 +708,23 @@ are enabled): * `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16. * `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h. +### Firebase limits +If [Firebase is configured](#firebase-fcm), all messages are also published to a Firebase topic (unless `Firebase: no` +is set). Firebase enforces [its own limits](https://firebase.google.com/docs/cloud-messaging/concept-options#topics_throttling) +on how many messages can be published. Unfortunately these limits are a little vague and can change depending on the time +of day. In practice, I have only ever observed `429 Quota exceeded` responses from Firebase if **too many messages are published to +the same topic**. + +In ntfy, if Firebase responds with a 429 after publishing to a topic, the visitor (= IP address) who published the message +is **banned from publishing to Firebase for 10 minutes** (not configurable). Because publishing to Firebase happens asynchronously, +there is no indication of the user that this has happened. Non-Firebase subscribers (WebSocket or HTTP stream) are not affected. +After the 10 minutes are up, messages forwarding to Firebase is resumed for this visitor. + +If this ever happens, there will be a log message that looks something like this: +``` +WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor +``` + ## Tuning for scale If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config, if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**. @@ -807,6 +824,26 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a maxretry = 10 ``` +## Debugging/tracing +If something's not working right, you can debug/trace through what the ntfy server is doing by setting the `log-level` +to `DEBUG` or `TRACE`. The `DEBUG` setting will output information about each published message, but not the message +contents. The `TRACE` setting will also print the message contents. + +!!! warning + Both options are very verbose and should only be enabled in production for short periods of time. Otherwise, + you're going to run out of disk space pretty quickly. + +You can also hot-reload the `log-level` by sending the `SIGHUP` signal to the process (or by calling +`systemctl reload ntfy` if it's running inside systemd). You can do so by calling `kill -HUP $(pidof ntfy)`. +If succesfull, you'll see something like this: + +``` +$ ntfy serve +2022/06/02 10:29:28 INFO Listening on :2586[http] :1025[smtp], log level is INFO +2022/06/02 10:29:34 INFO Partially hot reloading configuration ... +2022/06/02 10:29:34 INFO Log level is TRACE +``` + ## Config options Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `listen-http: :80`) or as a CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment @@ -817,43 +854,44 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). `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 | -|--------------------------------------------|-------------------------------------------------|-----------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) | -| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | -| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | -| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on | -| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | -| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | -| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | -| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | -| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | -| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | -| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | -| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | -| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | -| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | -| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | -| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | -| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | -| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | -| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | -| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | -| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | -| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | -| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | -| `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. | -| `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. | -| `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-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | -| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | -| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | -| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | -| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor | -| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | +| 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`) | +| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | +| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | +| `listen-unix` | `NTFY_LISTEN_UNIX` | *filename* | - | Path to a Unix socket to listen on | +| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | +| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. | +| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). | +| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | +| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | +| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). | +| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. | +| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | +| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. | +| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. | +| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. | +| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | +| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | +| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | +| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | +| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | +| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | +| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | +| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | +| `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. | +| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. | +| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers | +| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. | +| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | +| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor | +| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | +| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | +| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | +| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting | +| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | +| `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) | The format for a *duration* is: `(smh)`, e.g. 30s, 20m or 1h. The format for a *size* is: `(GMK)`, e.g. 1G, 200M or 4000k. @@ -881,42 +919,46 @@ DESCRIPTION: ntfy serve --listen-http :8080 # Starts server with alternate port OPTIONS: - --config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] + --attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR] + --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] + --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-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] + --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] + --auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE] --base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL] + --behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] + --cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION] + --cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE] + --cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE] + --config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] + --debug, -d enable debug logging (default: false) [$NTFY_DEBUG] + --firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE] + --global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT] + --keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL] + --key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE] --listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP] --listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS] --listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX] - --key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE] - --cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_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, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE] - --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, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE] - --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, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR] - --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, --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, --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, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL] + --log-level value, --log_level value set log level (default: "INFO") [$NTFY_LOG_LEVEL] --manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL] - --web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT] + --no-log-dates, --no_log_dates disable the date/time prefix (default: false) [$NTFY_NO_LOG_DATES] --smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR] - --smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER] - --smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS] --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_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN] - --smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN] + --smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS] + --smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER] --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, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT] - --visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT] - --visitor-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] + --smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN] + --smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN] + --trace enable tracing (very verbose, be careful) (default: false) [$NTFY_TRACE] + --upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL] --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, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST] - --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, --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-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-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST] --visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH] - --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) + --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-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-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-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT] + --web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT] ``` diff --git a/go.mod b/go.mod index 3c2fed05..6c36e8fc 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,10 @@ require ( github.com/urfave/cli/v2 v2.8.1 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 // indirect - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 golang.org/x/time v0.0.0-20220411224347-583f2d630306 - google.golang.org/api v0.81.0 + google.golang.org/api v0.82.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -44,14 +44,14 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.23.0 // indirect - golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect + golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine/v2 v2.0.1 // indirect - google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect - google.golang.org/grpc v1.46.2 // indirect + google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect + google.golang.org/grpc v1.47.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index a1881103..e0f0204e 100644 --- a/go.sum +++ b/go.sum @@ -341,9 +341,9 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-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/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -377,8 +377,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -560,8 +561,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR 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/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw= +google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -655,10 +656,10 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-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/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM= +google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -688,8 +689,9 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/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= diff --git a/log/log.go b/log/log.go index f105f720..5dac2327 100644 --- a/log/log.go +++ b/log/log.go @@ -83,6 +83,11 @@ func SetLevel(newLevel Level) { level = newLevel } +// DisableDates disables the date/time prefix +func DisableDates() { + log.SetFlags(0) +} + // ToLevel converts a string to a Level. It returns InfoLevel if the string // does not match any known log levels. func ToLevel(s string) Level { diff --git a/server/ntfy.service b/server/ntfy.service index f32ed898..8bf250a5 100644 --- a/server/ntfy.service +++ b/server/ntfy.service @@ -5,7 +5,7 @@ After=network.target [Service] User=ntfy Group=ntfy -ExecStart=/usr/bin/ntfy serve +ExecStart=/usr/bin/ntfy serve --no-log-dates ExecReload=/bin/kill --signal HUP $MAINPID Restart=on-failure AmbientCapabilities=CAP_NET_BIND_SERVICE diff --git a/server/server.go b/server/server.go index 98299213..ebbc118e 100644 --- a/server/server.go +++ b/server/server.go @@ -490,7 +490,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) { func (s *Server) sendEmail(v *visitor, m *message, email string) { log.Debug("%s Sending email to %s", logMessagePrefix(v, m), email) if err := s.smtpSender.Send(v, m, email); err != nil { - log.Warn("%s Unable to send email: %v", logMessagePrefix(v, m), err.Error()) + log.Warn("%s Unable to send email to %s: %v", logMessagePrefix(v, m), email, err.Error()) } } diff --git a/web/package-lock.json b/web/package-lock.json index 9853d848..a7671dba 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2004,6 +2004,24 @@ "postcss": "^8.3" } }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.1.tgz", + "integrity": "sha512-G78CY/+GePc6dDCTUbwI6TTFQ5fs3N9POHhI6v0QzteGpf6ylARiJUNz9HrRKi4eVYBNXjae1W2766iUEFxHlw==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/postcss-unset-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", @@ -3854,9 +3872,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "node_modules/@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", + "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3889,9 +3907,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.10.tgz", + "integrity": "sha512-dIugadZuIPrRzvIEevIu7A1smqOAjkSMv8qOfwPt9Ve6i6JT/FQcCHyk2qIAxwsQNKZt5/oGR0T4z9h2dXRAkg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5357,9 +5375,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==", + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==", "funding": [ { "type": "opencollective", @@ -5719,9 +5737,9 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "node_modules/core-js": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz", - "integrity": "sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", + "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -5729,9 +5747,9 @@ } }, "node_modules/core-js-compat": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.7.tgz", - "integrity": "sha512-uI9DAQKKiiE/mclIC5g4AjRpio27g+VMRhe6rQoz+q4Wm4L6A/fJhiLtBw+sfOpDG9wZ3O0pxIw7GbfOlBgjOA==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.8.tgz", + "integrity": "sha512-pQnwg4xtuvc2Bs/5zYQPaEYYSuTxsF7LBWF0SvnVhthZo/Qe+rJpcEekrdNK5DWwDJ0gv0oI9NNX5Mppdy0ctg==", "dependencies": { "browserslist": "^4.20.3", "semver": "7.0.0" @@ -5750,9 +5768,9 @@ } }, "node_modules/core-js-pure": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.7.tgz", - "integrity": "sha512-wTriFxiZI+C8msGeh7fJcbC/a0V8fdInN1oS2eK79DMBGs8iIJiXhtFJCiT3rBa8w6zroHWW3p8ArlujZ/Mz+w==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.8.tgz", + "integrity": "sha512-bOxbZIy9S5n4OVH63XaLVXZ49QKicjowDx/UELyJ68vxfCRpYsbyh/WNZNfEfAk+ekA8vSjt+gCDpvh672bc3w==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -6617,9 +6635,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.142", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.142.tgz", - "integrity": "sha512-ea8Q1YX0JRp4GylOmX4gFHIizi0j9GfRW4EkaHnkZp0agRCBB4ZGeCv17IEzIvBkiYVwfoKVhKZJbTfqCRdQdg==" + "version": "1.4.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.144.tgz", + "integrity": "sha512-R3RV3rU1xWwFJlSClVWDvARaOk6VUO/FubHLodIASDB3Mc2dzuWvNdfOgH9bwHUTqT79u92qw60NWfwUdzAqdg==" }, "node_modules/emittery": { "version": "0.8.1", @@ -6835,7 +6853,7 @@ "node_modules/escodegen/node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "engines": { "node": ">= 0.8.0" } @@ -7012,7 +7030,7 @@ "node_modules/eslint-module-utils/node_modules/p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dependencies": { "p-limit": "^1.1.0" }, @@ -7023,7 +7041,7 @@ "node_modules/eslint-module-utils/node_modules/p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "engines": { "node": ">=4" } @@ -7031,7 +7049,7 @@ "node_modules/eslint-module-utils/node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { "node": ">=4" } @@ -8635,9 +8653,9 @@ } }, "node_modules/i18next": { - "version": "21.8.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.5.tgz", - "integrity": "sha512-uI5LVG10SBHLVOclr6yY1aCimmrzeZ0dwD73Sio61E8gQEwRmKI7/M8RKM084mNNy7VscKtxzSwELrso8BKv1g==", + "version": "21.8.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.6.tgz", + "integrity": "sha512-tD0umB5lcYXJiD06m/XOEgdvDkFD17m13BP2tmSLralYhGPdmmPK6rErcmhc37t/6mubKdso30NLpPIomHaKnw==", "funding": [ { "type": "individual", @@ -11818,7 +11836,7 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node_modules/node-releases": { "version": "2.0.5", @@ -11836,7 +11854,7 @@ "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "engines": { "node": ">=0.10.0" } @@ -11882,7 +11900,7 @@ "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } @@ -12029,7 +12047,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } @@ -12198,7 +12216,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } @@ -12219,7 +12237,7 @@ "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -12232,7 +12250,7 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -12379,7 +12397,7 @@ "node_modules/pkg-up/node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { "node": ">=4" } @@ -13271,11 +13289,11 @@ } }, "node_modules/postcss-preset-env": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.6.0.tgz", - "integrity": "sha512-5cnzpSFZnQJOlBu85xn4Nnluy/WjIST/ugn+gOVcKnmFJ+GLtkfRhmJPo/TW9UDpG7oyA467kvDOO8mtcpOL4g==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.0.tgz", + "integrity": "sha512-2Q9YARQju+j2BVgAyDnW1pIWIMlaHZqbaGISPMmalznNlWcNFIZFQsJfRLXS+WHmHJDCmV7wIWpVf9JNKR4Elw==", "dependencies": { - "@csstools/postcss-cascade-layers": "^1.0.1", + "@csstools/postcss-cascade-layers": "^1.0.2", "@csstools/postcss-color-function": "^1.1.0", "@csstools/postcss-font-format-keywords": "^1.0.0", "@csstools/postcss-hwb-function": "^1.0.1", @@ -13285,16 +13303,17 @@ "@csstools/postcss-oklab-function": "^1.1.0", "@csstools/postcss-progressive-custom-properties": "^1.3.0", "@csstools/postcss-stepped-value-functions": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.0", "@csstools/postcss-unset-value": "^1.0.1", "autoprefixer": "^10.4.7", "browserslist": "^4.20.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", + "cssdb": "^6.6.2", "postcss-attribute-case-insensitive": "^5.0.0", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", + "postcss-color-functional-notation": "^4.2.3", "postcss-color-hex-alpha": "^8.0.3", "postcss-color-rebeccapurple": "^7.0.2", "postcss-custom-media": "^8.0.0", @@ -13312,7 +13331,7 @@ "postcss-lab-function": "^4.2.0", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.6", + "postcss-nesting": "^10.1.7", "postcss-opacity-percentage": "^1.1.2", "postcss-overflow-shorthand": "^3.0.3", "postcss-page-break": "^3.0.4", @@ -13626,7 +13645,7 @@ "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -14381,9 +14400,9 @@ } }, "node_modules/rollup": { - "version": "2.75.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.4.tgz", - "integrity": "sha512-JgZiJMJkKImMZJ8ZY1zU80Z2bA/TvrL/7D9qcBCrfl2bP+HUaIw0QHUroB4E3gBpFl6CRFM1YxGbuYGtdAswbQ==", + "version": "2.75.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.5.tgz", + "integrity": "sha512-JzNlJZDison3o2mOxVmb44Oz7t74EfSd1SQrplQk0wSaXV7uLQXtVdHbxlcT3w+8tZ1TL4r/eLfc7nAbz38BBA==", "bin": { "rollup": "dist/bin/rollup" }, @@ -15421,14 +15440,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", + "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.7", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", "terser": "^5.7.2" }, "engines": { @@ -15453,14 +15472,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -15891,9 +15902,9 @@ } }, "node_modules/watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -15919,9 +15930,9 @@ } }, "node_modules/webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -16036,9 +16047,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.1.tgz", + "integrity": "sha512-CTMfu2UMdR/4OOZVHRpdy84pNopOuigVIsRbGX3LVDMWNP8EUgC5mUBMErbwBlHTEX99ejZJpVqrir6EXAEajA==", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -16064,7 +16075,7 @@ "schema-utils": "^4.0.0", "selfsigned": "^2.0.1", "serve-index": "^1.9.1", - "sockjs": "^0.3.21", + "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", "ws": "^8.4.2" @@ -18067,6 +18078,14 @@ "postcss-value-parser": "^4.2.0" } }, + "@csstools/postcss-trigonometric-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.1.tgz", + "integrity": "sha512-G78CY/+GePc6dDCTUbwI6TTFQ5fs3N9POHhI6v0QzteGpf6ylARiJUNz9HrRKi4eVYBNXjae1W2766iUEFxHlw==", + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, "@csstools/postcss-unset-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.1.tgz", @@ -19347,9 +19366,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { - "version": "17.0.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz", - "integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==" + "version": "17.0.38", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", + "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" }, "@types/parse-json": { "version": "4.0.0", @@ -19382,9 +19401,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.9.tgz", - "integrity": "sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.10.tgz", + "integrity": "sha512-dIugadZuIPrRzvIEevIu7A1smqOAjkSMv8qOfwPt9Ve6i6JT/FQcCHyk2qIAxwsQNKZt5/oGR0T4z9h2dXRAkg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -20486,9 +20505,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001344", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz", - "integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==" + "version": "1.0.30001346", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001346.tgz", + "integrity": "sha512-q6ibZUO2t88QCIPayP/euuDREq+aMAxFE5S70PkrLh0iTDj/zEhgvJRKC2+CvXY6EWc6oQwUR48lL5vCW6jiXQ==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -20759,14 +20778,14 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, "core-js": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.7.tgz", - "integrity": "sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==" + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.8.tgz", + "integrity": "sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==" }, "core-js-compat": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.7.tgz", - "integrity": "sha512-uI9DAQKKiiE/mclIC5g4AjRpio27g+VMRhe6rQoz+q4Wm4L6A/fJhiLtBw+sfOpDG9wZ3O0pxIw7GbfOlBgjOA==", + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.22.8.tgz", + "integrity": "sha512-pQnwg4xtuvc2Bs/5zYQPaEYYSuTxsF7LBWF0SvnVhthZo/Qe+rJpcEekrdNK5DWwDJ0gv0oI9NNX5Mppdy0ctg==", "requires": { "browserslist": "^4.20.3", "semver": "7.0.0" @@ -20780,9 +20799,9 @@ } }, "core-js-pure": { - "version": "3.22.7", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.7.tgz", - "integrity": "sha512-wTriFxiZI+C8msGeh7fJcbC/a0V8fdInN1oS2eK79DMBGs8iIJiXhtFJCiT3rBa8w6zroHWW3p8ArlujZ/Mz+w==" + "version": "3.22.8", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.22.8.tgz", + "integrity": "sha512-bOxbZIy9S5n4OVH63XaLVXZ49QKicjowDx/UELyJ68vxfCRpYsbyh/WNZNfEfAk+ekA8vSjt+gCDpvh672bc3w==" }, "core-util-is": { "version": "1.0.3", @@ -21393,9 +21412,9 @@ } }, "electron-to-chromium": { - "version": "1.4.142", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.142.tgz", - "integrity": "sha512-ea8Q1YX0JRp4GylOmX4gFHIizi0j9GfRW4EkaHnkZp0agRCBB4ZGeCv17IEzIvBkiYVwfoKVhKZJbTfqCRdQdg==" + "version": "1.4.144", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.144.tgz", + "integrity": "sha512-R3RV3rU1xWwFJlSClVWDvARaOk6VUO/FubHLodIASDB3Mc2dzuWvNdfOgH9bwHUTqT79u92qw60NWfwUdzAqdg==" }, "emittery": { "version": "0.8.1", @@ -21557,7 +21576,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" }, "source-map": { "version": "0.6.1", @@ -21773,7 +21792,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "requires": { "p-limit": "^1.1.0" } @@ -21781,12 +21800,12 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==" }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" } } }, @@ -22863,9 +22882,9 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "i18next": { - "version": "21.8.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.5.tgz", - "integrity": "sha512-uI5LVG10SBHLVOclr6yY1aCimmrzeZ0dwD73Sio61E8gQEwRmKI7/M8RKM084mNNy7VscKtxzSwELrso8BKv1g==", + "version": "21.8.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.6.tgz", + "integrity": "sha512-tD0umB5lcYXJiD06m/XOEgdvDkFD17m13BP2tmSLralYhGPdmmPK6rErcmhc37t/6mubKdso30NLpPIomHaKnw==", "requires": { "@babel/runtime": "^7.17.2" } @@ -25158,7 +25177,7 @@ "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, "node-releases": { "version": "2.0.5", @@ -25173,7 +25192,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, "normalize-url": { "version": "6.1.0", @@ -25204,7 +25223,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-hash": { "version": "3.0.0", @@ -25303,7 +25322,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -25424,7 +25443,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -25439,7 +25458,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, "path-type": { "version": "4.0.0", @@ -25449,7 +25468,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "picocolors": { "version": "1.0.0", @@ -25553,7 +25572,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" } } }, @@ -26053,11 +26072,11 @@ } }, "postcss-preset-env": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.6.0.tgz", - "integrity": "sha512-5cnzpSFZnQJOlBu85xn4Nnluy/WjIST/ugn+gOVcKnmFJ+GLtkfRhmJPo/TW9UDpG7oyA467kvDOO8mtcpOL4g==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.7.0.tgz", + "integrity": "sha512-2Q9YARQju+j2BVgAyDnW1pIWIMlaHZqbaGISPMmalznNlWcNFIZFQsJfRLXS+WHmHJDCmV7wIWpVf9JNKR4Elw==", "requires": { - "@csstools/postcss-cascade-layers": "^1.0.1", + "@csstools/postcss-cascade-layers": "^1.0.2", "@csstools/postcss-color-function": "^1.1.0", "@csstools/postcss-font-format-keywords": "^1.0.0", "@csstools/postcss-hwb-function": "^1.0.1", @@ -26067,16 +26086,17 @@ "@csstools/postcss-oklab-function": "^1.1.0", "@csstools/postcss-progressive-custom-properties": "^1.3.0", "@csstools/postcss-stepped-value-functions": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.0", "@csstools/postcss-unset-value": "^1.0.1", "autoprefixer": "^10.4.7", "browserslist": "^4.20.3", "css-blank-pseudo": "^3.0.3", "css-has-pseudo": "^3.0.4", "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^6.6.1", + "cssdb": "^6.6.2", "postcss-attribute-case-insensitive": "^5.0.0", "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.2", + "postcss-color-functional-notation": "^4.2.3", "postcss-color-hex-alpha": "^8.0.3", "postcss-color-rebeccapurple": "^7.0.2", "postcss-custom-media": "^8.0.0", @@ -26094,7 +26114,7 @@ "postcss-lab-function": "^4.2.0", "postcss-logical": "^5.0.4", "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.1.6", + "postcss-nesting": "^10.1.7", "postcss-opacity-percentage": "^1.1.2", "postcss-overflow-shorthand": "^3.0.3", "postcss-page-break": "^3.0.4", @@ -26319,7 +26339,7 @@ "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" }, "qs": { "version": "6.10.3", @@ -26857,9 +26877,9 @@ } }, "rollup": { - "version": "2.75.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.4.tgz", - "integrity": "sha512-JgZiJMJkKImMZJ8ZY1zU80Z2bA/TvrL/7D9qcBCrfl2bP+HUaIw0QHUroB4E3gBpFl6CRFM1YxGbuYGtdAswbQ==", + "version": "2.75.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.5.tgz", + "integrity": "sha512-JzNlJZDison3o2mOxVmb44Oz7t74EfSd1SQrplQk0wSaXV7uLQXtVdHbxlcT3w+8tZ1TL4r/eLfc7nAbz38BBA==", "requires": { "fsevents": "~2.3.2" } @@ -27656,22 +27676,15 @@ } }, "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", + "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", "requires": { + "@jridgewell/trace-mapping": "^0.3.7", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", "terser": "^5.7.2" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } } }, "test-exclude": { @@ -28000,9 +28013,9 @@ } }, "watchpack": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -28022,9 +28035,9 @@ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" }, "webpack": { - "version": "5.72.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.72.1.tgz", - "integrity": "sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==", + "version": "5.73.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.73.0.tgz", + "integrity": "sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==", "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -28118,9 +28131,9 @@ } }, "webpack-dev-server": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.0.tgz", - "integrity": "sha512-+Nlb39iQSOSsFv0lWUuUTim3jDQO8nhK3E68f//J2r5rIcp4lULHXz2oZ0UVdEeWXEh5lSzYUlzarZhDAeAVQw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.9.1.tgz", + "integrity": "sha512-CTMfu2UMdR/4OOZVHRpdy84pNopOuigVIsRbGX3LVDMWNP8EUgC5mUBMErbwBlHTEX99ejZJpVqrir6EXAEajA==", "requires": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -28146,7 +28159,7 @@ "schema-utils": "^4.0.0", "selfsigned": "^2.0.1", "serve-index": "^1.9.1", - "sockjs": "^0.3.21", + "sockjs": "^0.3.24", "spdy": "^4.0.2", "webpack-dev-middleware": "^5.3.1", "ws": "^8.4.2" From e12995e2184dd69c4ca8f2f2ff52c3a92dc61837 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 11:59:22 -0400 Subject: [PATCH 26/38] Logging in subscribe and publish command --- client/client.go | 17 ++++++++++++----- cmd/subscribe.go | 29 +++++++++++++++++++---------- docs/config.md | 6 +++--- docs/examples.md | 2 +- docs/releases.md | 12 ++++++------ server/message_cache.go | 16 ++++++++-------- server/server.go | 4 ++-- server/server_firebase.go | 3 ++- server/util.go | 12 ------------ util/util.go | 14 ++++++++++++++ 10 files changed, 67 insertions(+), 48 deletions(-) diff --git a/client/client.go b/client/client.go index eaff5673..242df4fa 100644 --- a/client/client.go +++ b/client/client.go @@ -7,9 +7,9 @@ import ( "encoding/json" "errors" "fmt" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" "io" - "log" "net/http" "strings" "sync" @@ -102,6 +102,7 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO return nil, err } } + log.Debug("%s Publishing message with headers %s", util.ShortTopicURL(topicURL), req.Header) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -136,6 +137,7 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err msgChan := make(chan *Message) errChan := make(chan error) topicURL := c.expandTopicURL(topic) + log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL)) options = append(options, WithPoll()) go func() { err := performSubscribeRequest(ctx, msgChan, topicURL, "", options...) @@ -171,6 +173,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string { defer c.mu.Unlock() subscriptionID := util.RandomString(10) topicURL := c.expandTopicURL(topic) + log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL)) ctx, cancel := context.WithCancel(context.Background()) c.subscriptions[subscriptionID] = &subscription{ ID: subscriptionID, @@ -226,11 +229,11 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR // TODO The retry logic is crude and may lose messages. It should record the last message like the // Android client, use since=, and do incremental backoff too if err := performSubscribeRequest(ctx, msgChan, topicURL, subcriptionID, options...); err != nil { - log.Printf("Connection to %s failed: %s", topicURL, err.Error()) + log.Warn("%s Connection failed: %s", util.ShortTopicURL(topicURL), err.Error()) } select { case <-ctx.Done(): - log.Printf("Connection to %s exited", topicURL) + log.Info("Connection exited", util.ShortTopicURL(topicURL)) return case <-time.After(10 * time.Second): // TODO Add incremental backoff } @@ -238,7 +241,9 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR } func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, subscriptionID string, options ...SubscribeOption) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil) + streamURL := fmt.Sprintf("%s/json", topicURL) + log.Debug("%s Listening to %s", util.ShortTopicURL(topicURL), streamURL) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, streamURL, nil) if err != nil { return err } @@ -261,10 +266,12 @@ func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicUR } scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { - m, err := toMessage(scanner.Text(), topicURL, subscriptionID) + messageJSON := scanner.Text() + m, err := toMessage(messageJSON, topicURL, subscriptionID) if err != nil { return err } + log.Trace("%s Message received: %s", util.ShortTopicURL(topicURL), messageJSON) if m.Event == MessageEvent { msgChan <- m } diff --git a/cmd/subscribe.go b/cmd/subscribe.go index 3b469344..3cab077c 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -5,12 +5,13 @@ import ( "fmt" "github.com/urfave/cli/v2" "heckel.io/ntfy/client" + "heckel.io/ntfy/log" "heckel.io/ntfy/util" - "log" "os" "os/exec" "os/user" "path/filepath" + "sort" "strings" ) @@ -32,7 +33,6 @@ var flagsSubscribe = append( &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"}, &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"}, &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, - &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"}, ) var cmdSubscribe = &cli.Command{ @@ -190,6 +190,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, if !ok { continue } + log.Debug("%s Dispatching received message: %s", logMessagePrefix(m), m.Raw) printMessageOrRunCommand(c, m, cmd) } return nil @@ -199,26 +200,26 @@ func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) if command != "" { runCommand(c, command, m) } else { + log.Debug("%s Printing raw message", logMessagePrefix(m)) fmt.Fprintln(c.App.Writer, m.Raw) } } func runCommand(c *cli.Context, command string, m *client.Message) { if err := runCommandInternal(c, command, m); err != nil { - fmt.Fprintf(c.App.ErrWriter, "Command failed: %s\n", err.Error()) + log.Warn("%s Command failed: %s", logMessagePrefix(m), err.Error()) } } func runCommandInternal(c *cli.Context, script string, m *client.Message) error { scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.%s", os.TempDir(), util.RandomString(10), scriptExt) - if err := os.WriteFile(scriptFile, []byte(scriptHeader+script), 0700); err != nil { + log.Debug("%s Running command '%s' via temporary script %s", logMessagePrefix(m), script, scriptFile) + script = scriptHeader + script + if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil { return err } defer os.Remove(scriptFile) - verbose := c.Bool("verbose") - if verbose { - log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), script, m.Raw) - } + log.Debug("%s Executing script %s", logMessagePrefix(m), scriptFile) cmd := exec.Command(scriptLauncher[0], append(scriptLauncher[1:], scriptFile)...) cmd.Stdin = c.App.Reader cmd.Stdout = c.App.Writer @@ -228,7 +229,7 @@ func runCommandInternal(c *cli.Context, script string, m *client.Message) error } func envVars(m *client.Message) []string { - env := os.Environ() + env := make([]string, 0) env = append(env, envVar(m.ID, "NTFY_ID", "id")...) env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...) env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...) @@ -237,7 +238,11 @@ func envVars(m *client.Message) []string { env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...) env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "tag", "ta")...) env = append(env, envVar(m.Raw, "NTFY_RAW", "raw")...) - return env + sort.Strings(env) + if log.IsTrace() { + log.Trace("%s With environment:\n%s", logMessagePrefix(m), strings.Join(env, "\n")) + } + return append(os.Environ(), env...) } func envVar(value string, vars ...string) []string { @@ -276,3 +281,7 @@ func defaultClientConfigFileWindows() string { homeDir, _ := os.UserConfigDir() return filepath.Join(homeDir, clientUserConfigFileWindowsRelative) } + +func logMessagePrefix(m *client.Message) string { + return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID) +} diff --git a/docs/config.md b/docs/config.md index fc1c115e..db664c7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -833,9 +833,9 @@ contents. The `TRACE` setting will also print the message contents. Both options are very verbose and should only be enabled in production for short periods of time. Otherwise, you're going to run out of disk space pretty quickly. -You can also hot-reload the `log-level` by sending the `SIGHUP` signal to the process (or by calling -`systemctl reload ntfy` if it's running inside systemd). You can do so by calling `kill -HUP $(pidof ntfy)`. -If succesfull, you'll see something like this: +You can also hot-reload the `log-level` by sending the `SIGHUP` signal to the process after editing the `server.yml` file. +You can do so by calling `systemctl reload ntfy` (if ntfy is running inside systemd), or by calling `kill -HUP $(pidof ntfy)`. +If successful, you'll see something like this: ``` $ ntfy serve diff --git a/docs/examples.md b/docs/examples.md index 8c94daf6..26be6b30 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -44,7 +44,7 @@ fi ## Server-sent messages in your web app Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own -web application. Check out the live example or just look the source of this page. +web application. Check out the live example. ## Notify on SSH login Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I diff --git a/docs/releases.md b/docs/releases.md index a8d3fe10..7b6d566f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -10,12 +10,14 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) +--> -## ntfy server v1.25.0 (UNRELEASED) +## ntfy server v1.25.0 +Released June 2, 2022 **Features:** -* Advanced logging, with different log levels and hot reloading of the log level (no ticket) +* Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284)) **Bugs**: @@ -28,18 +30,16 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Documentation**: +* ⚠️ [Privacy policy](privacy.md) updated to reflect additional debug/tracing feature (no ticket) * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) * Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) * Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting) -* Example for using ntfy with docker-compose.yml without root privileges ([#304](https://github.com/binwiederhier/ntfy/pull/304), thanks to [@ksurl](https://github.com/ksurl)) +* Example for using ntfy with docker-compose.yml without root privileges ([#304](https://github.com/binwiederhier/ntfy/pull/304), thanks to [@ksurl](https://github.com/ksurl)) **Additional translations:** * Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/)) ---> - - ## ntfy iOS app v1.1 Released May 31, 2022 diff --git a/server/message_cache.go b/server/message_cache.go index 4dc83bdf..77aa4f78 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -6,8 +6,8 @@ import ( "errors" "fmt" _ "github.com/mattn/go-sqlite3" // SQLite driver + "heckel.io/ntfy/log" "heckel.io/ntfy/util" - "log" "strings" "time" ) @@ -540,7 +540,7 @@ func setupNewCacheDB(db *sql.DB) error { } func migrateFrom0(db *sql.DB) error { - log.Print("Migrating cache database schema: from 0 to 1") + log.Info("Migrating cache database schema: from 0 to 1") if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil { return err } @@ -554,7 +554,7 @@ func migrateFrom0(db *sql.DB) error { } func migrateFrom1(db *sql.DB) error { - log.Print("Migrating cache database schema: from 1 to 2") + log.Info("Migrating cache database schema: from 1 to 2") if _, err := db.Exec(migrate1To2AlterMessagesTableQuery); err != nil { return err } @@ -565,7 +565,7 @@ func migrateFrom1(db *sql.DB) error { } func migrateFrom2(db *sql.DB) error { - log.Print("Migrating cache database schema: from 2 to 3") + log.Info("Migrating cache database schema: from 2 to 3") if _, err := db.Exec(migrate2To3AlterMessagesTableQuery); err != nil { return err } @@ -576,7 +576,7 @@ func migrateFrom2(db *sql.DB) error { } func migrateFrom3(db *sql.DB) error { - log.Print("Migrating cache database schema: from 3 to 4") + log.Info("Migrating cache database schema: from 3 to 4") if _, err := db.Exec(migrate3To4AlterMessagesTableQuery); err != nil { return err } @@ -587,7 +587,7 @@ func migrateFrom3(db *sql.DB) error { } func migrateFrom4(db *sql.DB) error { - log.Print("Migrating cache database schema: from 4 to 5") + log.Info("Migrating cache database schema: from 4 to 5") if _, err := db.Exec(migrate4To5AlterMessagesTableQuery); err != nil { return err } @@ -598,7 +598,7 @@ func migrateFrom4(db *sql.DB) error { } func migrateFrom5(db *sql.DB) error { - log.Print("Migrating cache database schema: from 5 to 6") + log.Info("Migrating cache database schema: from 5 to 6") if _, err := db.Exec(migrate5To6AlterMessagesTableQuery); err != nil { return err } @@ -609,7 +609,7 @@ func migrateFrom5(db *sql.DB) error { } func migrateFrom6(db *sql.DB) error { - log.Print("Migrating cache database schema: from 6 to 7") + log.Info("Migrating cache database schema: from 6 to 7") if _, err := db.Exec(migrate6To7AlterMessagesTableQuery); err != nil { return err } diff --git a/server/server.go b/server/server.go index ebbc118e..a26fcc38 100644 --- a/server/server.go +++ b/server/server.go @@ -253,7 +253,7 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) { if isNormalError { log.Debug("%s WebSocket error (this error is okay, it happens a lot): %s", logHTTPPrefix(v, r), err.Error()) } else { - log.Warn("%s WebSocket error: %s", logHTTPPrefix(v, r), err.Error()) + log.Info("%s WebSocket error: %s", logHTTPPrefix(v, r), err.Error()) } return // Do not attempt to write to upgraded connection } @@ -446,7 +446,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito log.Debug("%s Received message: event=%s, body=%d byte(s), delayed=%t, firebase=%t, cache=%t, up=%t, email=%s", logMessagePrefix(v, m), m.Event, len(m.Message), delayed, firebase, cache, unifiedpush, email) if log.IsTrace() { - log.Trace("%s Message body: %s", logMessagePrefix(v, m), maybeMarshalJSON(m)) + log.Trace("%s Message body: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(m)) } if !delayed { if err := t.Publish(v, m); err != nil { diff --git a/server/server_firebase.go b/server/server_firebase.go index 2abc10de..e622a555 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -10,6 +10,7 @@ import ( "google.golang.org/api/option" "heckel.io/ntfy/auth" "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "strings" ) @@ -45,7 +46,7 @@ func (c *firebaseClient) Send(v *visitor, m *message) error { return err } if log.IsTrace() { - log.Trace("%s Firebase message: %s", logMessagePrefix(v, m), maybeMarshalJSON(fbm)) + log.Trace("%s Firebase message: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(fbm)) } err = c.sender.Send(fbm) if err == errFirebaseQuotaExceeded { diff --git a/server/util.go b/server/util.go index ffd07d15..d6b95a73 100644 --- a/server/util.go +++ b/server/util.go @@ -1,7 +1,6 @@ package server import ( - "encoding/json" "fmt" "github.com/emersion/go-smtp" "net/http" @@ -59,14 +58,3 @@ func logHTTPPrefix(v *visitor, r *http.Request) string { func logSMTPPrefix(state *smtp.ConnectionState) string { return fmt.Sprintf("%s/%s SMTP", state.Hostname, state.RemoteAddr.String()) } - -func maybeMarshalJSON(v interface{}) string { - messageJSON, err := json.MarshalIndent(v, "", " ") - if err != nil { - return "" - } - if len(messageJSON) > 5000 { - return string(messageJSON)[:5000] - } - return string(messageJSON) -} diff --git a/util/util.go b/util/util.go index 7fc22d09..c8a42347 100644 --- a/util/util.go +++ b/util/util.go @@ -2,6 +2,7 @@ package util import ( "encoding/base64" + "encoding/json" "errors" "fmt" "github.com/gabriel-vasile/mimetype" @@ -264,3 +265,16 @@ func ReadPassword(in io.Reader) ([]byte, error) { func BasicAuth(user, pass string) string { return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass)))) } + +// MaybeMarshalJSON returns a JSON string of the given object, or "" if serialization failed. +// This is useful for logging purposes where a failure doesn't matter that much. +func MaybeMarshalJSON(v interface{}) string { + jsonBytes, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "" + } + if len(jsonBytes) > 5000 { + return string(jsonBytes)[:5000] + } + return string(jsonBytes) +} From 39b1de33207274c19c942917cf932f6cdff634c7 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 14:38:38 -0400 Subject: [PATCH 27/38] Fix log --- client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client.go b/client/client.go index 242df4fa..8b05a393 100644 --- a/client/client.go +++ b/client/client.go @@ -233,7 +233,7 @@ func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicUR } select { case <-ctx.Done(): - log.Info("Connection exited", util.ShortTopicURL(topicURL)) + log.Info("%s Connection exited", util.ShortTopicURL(topicURL)) return case <-time.After(10 * time.Second): // TODO Add incremental backoff } From 850c6725f580b2367dc5d4f3c427104acb074113 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 14:40:19 -0400 Subject: [PATCH 28/38] Remove new line --- server/server.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/server/server.yml b/server/server.yml index 2d62b704..63c32209 100644 --- a/server/server.yml +++ b/server/server.yml @@ -51,7 +51,6 @@ # cache-file: # cache-duration: "12h" - # If set, access to the ntfy server and API can be controlled on a granular level using # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs. # From 747587971254c8dccf9af7adcc2f7c5539dbcec6 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 14:45:36 -0400 Subject: [PATCH 29/38] Added Dutch --- docs/releases.md | 1 + web/package-lock.json | 12 ++++++------ web/src/components/Preferences.js | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 7b6d566f..255f63c2 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -39,6 +39,7 @@ Released June 2, 2022 **Additional translations:** * Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/)) +* Dutch (thanks to [@SchoNie](https://hosted.weblate.org/user/SchoNie/)) ## ntfy iOS app v1.1 Released May 31, 2022 diff --git a/web/package-lock.json b/web/package-lock.json index a7671dba..505f2c63 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8653,9 +8653,9 @@ } }, "node_modules/i18next": { - "version": "21.8.6", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.6.tgz", - "integrity": "sha512-tD0umB5lcYXJiD06m/XOEgdvDkFD17m13BP2tmSLralYhGPdmmPK6rErcmhc37t/6mubKdso30NLpPIomHaKnw==", + "version": "21.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.7.tgz", + "integrity": "sha512-fYbEmaCIMSDrAzXGnisZyBd0h3bcO43jVJa5fulk4ambTIOgs9tqgNsiyr1sy6Xi4iZpYMHp2ZBjU3IcCLGxPA==", "funding": [ { "type": "individual", @@ -22882,9 +22882,9 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "i18next": { - "version": "21.8.6", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.6.tgz", - "integrity": "sha512-tD0umB5lcYXJiD06m/XOEgdvDkFD17m13BP2tmSLralYhGPdmmPK6rErcmhc37t/6mubKdso30NLpPIomHaKnw==", + "version": "21.8.7", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.7.tgz", + "integrity": "sha512-fYbEmaCIMSDrAzXGnisZyBd0h3bcO43jVJa5fulk4ambTIOgs9tqgNsiyr1sy6Xi4iZpYMHp2ZBjU3IcCLGxPA==", "requires": { "@babel/runtime": "^7.17.2" } diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index e2899ecc..468cc4c3 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -436,7 +436,7 @@ const Appearance = () => { const Language = () => { const { t, i18n } = useTranslation(); const labelId = "prefLanguage"; - const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); + const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "🇭🇺", "🇧🇷", "🇳🇱", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" "); const lang = i18n.language ?? "en"; @@ -459,6 +459,7 @@ const Language = () => { Italiano Magyar 日本語 + Nederlands Norsk bokmål Português (Brasil) Русский From 1e16899ae3c6e681e6c6562991faa101dc5cb68f Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 14:46:42 -0400 Subject: [PATCH 30/38] Bump install docs --- docs/install.md | 58 ++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/install.md b/docs/install.md index 595bbd9f..6489d890 100644 --- a/docs/install.md +++ b/docs/install.md @@ -26,37 +26,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_x86_64.tar.gz - tar zxvf ntfy_1.24.0_linux_x86_64.tar.gz - sudo cp -a ntfy_1.24.0_linux_x86_64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_x86_64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_x86_64.tar.gz + tar zxvf ntfy_1.25.0_linux_x86_64.tar.gz + sudo cp -a ntfy_1.25.0_linux_x86_64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_x86_64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.tar.gz - tar zxvf ntfy_1.24.0_linux_armv6.tar.gz - sudo cp -a ntfy_1.24.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.tar.gz + tar zxvf ntfy_1.25.0_linux_armv6.tar.gz + sudo cp -a ntfy_1.25.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.tar.gz - tar zxvf ntfy_1.24.0_linux_armv7.tar.gz - sudo cp -a ntfy_1.24.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.tar.gz + tar zxvf ntfy_1.25.0_linux_armv7.tar.gz + sudo cp -a ntfy_1.25.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.tar.gz - tar zxvf ntfy_1.24.0_linux_arm64.tar.gz - sudo cp -a ntfy_1.24.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.24.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.tar.gz + tar zxvf ntfy_1.25.0_linux_arm64.tar.gz + sudo cp -a ntfy_1.25.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -103,7 +103,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -111,7 +111,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -119,7 +119,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -127,7 +127,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -137,28 +137,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_1.24.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -190,11 +190,11 @@ If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For a `~/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 +curl https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_v1.25.0_macOS_all.tar.gz > ntfy_v1.25.0_macOS_all.tar.gz +tar zxvf ntfy_v1.25.0_macOS_all.tar.gz +sudo cp -a ntfy_v1.25.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 +cp ntfy_v1.25.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -206,7 +206,7 @@ ntfy --help ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.24.0/ntfy_v1.24.0_windows_x86_64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_v1.25.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). From f6dee345b705441b8aca865dc9dfafdfbee259c4 Mon Sep 17 00:00:00 2001 From: ksurl Date: Thu, 2 Jun 2022 11:58:59 -0700 Subject: [PATCH 31/38] add timezone for docker install --- docker-compose.yml | 1 + docs/install.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d4841bc0..2ab66a8c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy + - /etc/localtime:/etc/localtime:ro ports: - 80:80 restart: unless-stopped diff --git a/docs/install.md b/docs/install.md index 6489d890..8fe43baa 100644 --- a/docs/install.md +++ b/docs/install.md @@ -239,10 +239,11 @@ docker run \ serve ``` -With other config options and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): +With other config options, host-matched timezone, and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): ```bash docker run \ -v /etc/ntfy:/etc/ntfy \ + -v /etc/localtime:/etc/localtime:ro \ -p 80:80 \ -u UID:GID \ -it \ @@ -264,6 +265,7 @@ services: volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy + - /etc/localtime:/etc/localtime:ro # set timezone to match host ports: - 80:80 restart: unless-stopped From 887a7c32880bb7537561d238ba96f2e584af74a3 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 15:10:16 -0400 Subject: [PATCH 32/38] Changelog --- docs/releases.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index 255f63c2..eaaa0a1f 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -15,6 +15,13 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## ntfy server v1.25.0 Released June 2, 2022 +This release adds the ability to set a log level to facilitate easier debugging of live systems. It also solves a +production problem with a few over-users that resulted in Firebase quota problems (only applying to the over-users). +We now block visitors from using Firebase if they trigger a quota exceeded response. + +On top of that, we updated the Firebase SDK and are now building the release in GitHub Actions. We've also got two +more translations: Chinese/Simplified and Dutch. + **Features:** * Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284)) From ae3163c5b1c559442464f6c21d789a20f474252b Mon Sep 17 00:00:00 2001 From: ksurl Date: Thu, 2 Jun 2022 13:35:49 -0700 Subject: [PATCH 33/38] add tzdata to image and use env for docker timezone --- Dockerfile | 2 ++ docker-compose.yml | 5 +++-- docs/install.md | 9 +++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6916cabc..9a5e2b30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,7 @@ MAINTAINER Philipp C. Heckel COPY ntfy /usr/bin +RUN apk add --no-cache tzdata + EXPOSE 80/tcp ENTRYPOINT ["ntfy"] diff --git a/docker-compose.yml b/docker-compose.yml index 2ab66a8c..d39492e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,12 @@ services: container_name: ntfy command: - serve - user: UID:GID # optional. Set custom user/group or uid/gid + environment: + - TZ=UTC # optional: Change to your desired timezone + user: UID:GID # optional: Set custom user/group or uid/gid volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy - - /etc/localtime:/etc/localtime:ro ports: - 80:80 restart: unless-stopped diff --git a/docs/install.md b/docs/install.md index 8fe43baa..092c9a4b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -239,11 +239,11 @@ docker run \ serve ``` -With other config options, host-matched timezone, and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): +With other config options, timezone, and non-root user (configured via `/etc/ntfy/server.yml`, see [configuration](config.md) for details): ```bash docker run \ -v /etc/ntfy:/etc/ntfy \ - -v /etc/localtime:/etc/localtime:ro \ + -e TZ=UTC \ -p 80:80 \ -u UID:GID \ -it \ @@ -261,11 +261,12 @@ services: container_name: ntfy command: - serve - user: UID:GID # optional. replace with your own user/group or uid/gid + environment: + - TZ=UTC # optional: set desired timezone + user: UID:GID # optional: replace with your own user/group or uid/gid volumes: - /var/cache/ntfy:/var/cache/ntfy - /etc/ntfy:/etc/ntfy - - /etc/localtime:/etc/localtime:ro # set timezone to match host ports: - 80:80 restart: unless-stopped From 17930caf211757eef939416669002f4eb879f9d0 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 16:42:17 -0400 Subject: [PATCH 34/38] Changelog --- docs/releases.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releases.md b/docs/releases.md index eaaa0a1f..704a60ee 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -10,6 +10,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) +## ntfy Android app v1.26.0 (UNRELEASED) + +**Features:** + +* Add `tzdata` to Docker image to allow overriding the timezone with `TZ` ([#307](https://github.com/binwiederhier/ntfy/pull/307), thanks to [@ksurl](https://github.com/ksurl)) + --> ## ntfy server v1.25.0 From 0521f19ea4443937bc0569c3ff7f9d24c2abc111 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 20:59:07 -0400 Subject: [PATCH 35/38] Fix docs header color; tiny other fixes with logging --- docs/install.md | 58 +++++++++++++++++----------------- docs/releases.md | 10 ++---- docs/static/css/extra.css | 2 +- docs/static/js/extra.js | 8 ++--- server/server.go | 8 +++-- server/server_firebase.go | 5 +-- server/server_firebase_test.go | 2 +- 7 files changed, 47 insertions(+), 46 deletions(-) diff --git a/docs/install.md b/docs/install.md index 092c9a4b..5e9f8d44 100644 --- a/docs/install.md +++ b/docs/install.md @@ -26,37 +26,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_x86_64.tar.gz - tar zxvf ntfy_1.25.0_linux_x86_64.tar.gz - sudo cp -a ntfy_1.25.0_linux_x86_64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_x86_64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_x86_64.tar.gz + tar zxvf ntfy_1.25.1_linux_x86_64.tar.gz + sudo cp -a ntfy_1.25.1_linux_x86_64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_x86_64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.tar.gz - tar zxvf ntfy_1.25.0_linux_armv6.tar.gz - sudo cp -a ntfy_1.25.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.tar.gz + tar zxvf ntfy_1.25.1_linux_armv6.tar.gz + sudo cp -a ntfy_1.25.1_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.tar.gz - tar zxvf ntfy_1.25.0_linux_armv7.tar.gz - sudo cp -a ntfy_1.25.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.tar.gz + tar zxvf ntfy_1.25.1_linux_armv7.tar.gz + sudo cp -a ntfy_1.25.1_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.tar.gz - tar zxvf ntfy_1.25.0_linux_arm64.tar.gz - sudo cp -a ntfy_1.25.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.tar.gz + tar zxvf ntfy_1.25.1_linux_arm64.tar.gz + sudo cp -a ntfy_1.25.1_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -103,7 +103,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -111,7 +111,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -119,7 +119,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -127,7 +127,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -137,28 +137,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_1.25.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -190,11 +190,11 @@ If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For a `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_v1.25.0_macOS_all.tar.gz > ntfy_v1.25.0_macOS_all.tar.gz -tar zxvf ntfy_v1.25.0_macOS_all.tar.gz -sudo cp -a ntfy_v1.25.0_macOS_all/ntfy /usr/local/bin/ntfy +curl https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_v1.25.1_macOS_all.tar.gz > ntfy_v1.25.1_macOS_all.tar.gz +tar zxvf ntfy_v1.25.1_macOS_all.tar.gz +sudo cp -a ntfy_v1.25.1_macOS_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_v1.25.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_v1.25.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -206,7 +206,7 @@ ntfy --help ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.0/ntfy_v1.25.0_windows_x86_64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_v1.25.1_windows_x86_64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). diff --git a/docs/releases.md b/docs/releases.md index 704a60ee..3563f387 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -10,15 +10,9 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/)) -## ntfy Android app v1.26.0 (UNRELEASED) - -**Features:** - -* Add `tzdata` to Docker image to allow overriding the timezone with `TZ` ([#307](https://github.com/binwiederhier/ntfy/pull/307), thanks to [@ksurl](https://github.com/ksurl)) - --> -## ntfy server v1.25.0 +## ntfy server v1.25.1 Released June 2, 2022 This release adds the ability to set a log level to facilitate easier debugging of live systems. It also solves a @@ -31,10 +25,12 @@ more translations: Chinese/Simplified and Dutch. **Features:** * Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284)) +* Add `tzdata` to Docker image to allow overriding the timezone with `TZ` ([#307](https://github.com/binwiederhier/ntfy/pull/307), thanks to [@ksurl](https://github.com/ksurl)) **Bugs**: * Respect Firebase "quota exceeded" response for topics, block Firebase publishing for user for 10min ([#289](https://github.com/binwiederhier/ntfy/issues/289)) +* Fix documentation header blue header due to mkdocs-material theme update (no ticket) **Maintenance:** diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css index cb71a018..a7370399 100644 --- a/docs/static/css/extra.css +++ b/docs/static/css/extra.css @@ -1,4 +1,4 @@ -:root { +:root > * { --md-primary-fg-color: #338574; --md-primary-fg-color--light: #338574; --md-primary-fg-color--dark: #338574; diff --git a/docs/static/js/extra.js b/docs/static/js/extra.js index 0aa380a7..6ddf07a9 100644 --- a/docs/static/js/extra.js +++ b/docs/static/js/extra.js @@ -1,8 +1,8 @@ // Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs -const savedTab = localStorage.getItem('savedTab') -const tabs = document.querySelectorAll(".tabbed-set > input") -for (const tab of tabs) { +const savedCodeTab = localStorage.getItem('savedTab') +const codeTabs = document.querySelectorAll(".tabbed-set > input") +for (const tab of codeTabs) { tab.addEventListener("click", () => { const current = document.querySelector(`label[for=${tab.id}]`) const pos = current.getBoundingClientRect().top @@ -25,7 +25,7 @@ for (const tab of tabs) { // Select saved tab const current = document.querySelector(`label[for=${tab.id}]`) const labelContent = current.innerHTML - if (savedTab === labelContent) { + if (savedCodeTab === labelContent) { tab.checked = true } } diff --git a/server/server.go b/server/server.go index a26fcc38..0b909b80 100644 --- a/server/server.go +++ b/server/server.go @@ -261,7 +261,7 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) { if !ok { httpErr = errHTTPInternalError } - isNormalError := httpErr.HTTPCode == http.StatusNotFound + isNormalError := httpErr.HTTPCode == http.StatusNotFound || httpErr.HTTPCode == http.StatusBadRequest if isNormalError { log.Debug("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error()) } else { @@ -483,7 +483,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito func (s *Server) sendToFirebase(v *visitor, m *message) { log.Debug("%s Publishing to Firebase", logMessagePrefix(v, m)) if err := s.firebaseClient.Send(v, m); err != nil { - log.Warn("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) + if err == errFirebaseTemporarilyBanned { + log.Debug("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) + } else { + log.Warn("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error()) + } } } diff --git a/server/server_firebase.go b/server/server_firebase.go index e622a555..99f0ba13 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -20,7 +20,8 @@ const ( ) var ( - errFirebaseQuotaExceeded = errors.New("quota exceeded for Firebase messages to topic") + errFirebaseQuotaExceeded = errors.New("quota exceeded for Firebase messages to topic") + errFirebaseTemporarilyBanned = errors.New("visitor temporarily banned from using Firebase") ) // firebaseClient is a generic client that formats and sends messages to Firebase. @@ -39,7 +40,7 @@ func newFirebaseClient(sender firebaseSender, auther auth.Auther) *firebaseClien func (c *firebaseClient) Send(v *visitor, m *message) error { if err := v.FirebaseAllowed(); err != nil { - return errFirebaseQuotaExceeded + return errFirebaseTemporarilyBanned } fbm, err := toFirebaseMessage(m, c.auther) if err != nil { diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index b29cf3af..b004b0fa 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -331,6 +331,6 @@ func TestToFirebaseSender_Abuse(t *testing.T) { require.Equal(t, 2, len(sender.Messages())) sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working - require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) + require.Equal(t, errFirebaseTemporarilyBanned, client.Send(visitor, &message{Topic: "mytopic"})) require.Equal(t, 0, len(sender.Messages())) } From 86c132f9cd4717eaa3c18753ac0f6e44d7c903bc Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Thu, 2 Jun 2022 21:38:27 -0400 Subject: [PATCH 36/38] Revert tzdata change --- Dockerfile | 2 -- docs/install.md | 58 ++++++++++++++++++++++++------------------------ docs/releases.md | 3 +-- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9a5e2b30..6916cabc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,5 @@ MAINTAINER Philipp C. Heckel COPY ntfy /usr/bin -RUN apk add --no-cache tzdata - EXPOSE 80/tcp ENTRYPOINT ["ntfy"] diff --git a/docs/install.md b/docs/install.md index 5e9f8d44..210ca146 100644 --- a/docs/install.md +++ b/docs/install.md @@ -26,37 +26,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_x86_64.tar.gz - tar zxvf ntfy_1.25.1_linux_x86_64.tar.gz - sudo cp -a ntfy_1.25.1_linux_x86_64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_x86_64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_x86_64.tar.gz + tar zxvf ntfy_1.25.2_linux_x86_64.tar.gz + sudo cp -a ntfy_1.25.2_linux_x86_64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_x86_64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.tar.gz - tar zxvf ntfy_1.25.1_linux_armv6.tar.gz - sudo cp -a ntfy_1.25.1_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv6.tar.gz + tar zxvf ntfy_1.25.2_linux_armv6.tar.gz + sudo cp -a ntfy_1.25.2_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.tar.gz - tar zxvf ntfy_1.25.1_linux_armv7.tar.gz - sudo cp -a ntfy_1.25.1_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv7.tar.gz + tar zxvf ntfy_1.25.2_linux_armv7.tar.gz + sudo cp -a ntfy_1.25.2_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.tar.gz - tar zxvf ntfy_1.25.1_linux_arm64.tar.gz - sudo cp -a ntfy_1.25.1_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.1_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_arm64.tar.gz + tar zxvf ntfy_1.25.2_linux_arm64.tar.gz + sudo cp -a ntfy_1.25.2_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_1.25.2_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -103,7 +103,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -111,7 +111,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -119,7 +119,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -127,7 +127,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -137,28 +137,28 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_1.25.1_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_1.25.2_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` @@ -190,11 +190,11 @@ If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For a `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_v1.25.1_macOS_all.tar.gz > ntfy_v1.25.1_macOS_all.tar.gz -tar zxvf ntfy_v1.25.1_macOS_all.tar.gz -sudo cp -a ntfy_v1.25.1_macOS_all/ntfy /usr/local/bin/ntfy +curl https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_v1.25.2_macOS_all.tar.gz > ntfy_v1.25.2_macOS_all.tar.gz +tar zxvf ntfy_v1.25.2_macOS_all.tar.gz +sudo cp -a ntfy_v1.25.2_macOS_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_v1.25.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_v1.25.2_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -206,7 +206,7 @@ ntfy --help ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.1/ntfy_v1.25.1_windows_x86_64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.25.2/ntfy_v1.25.2_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). diff --git a/docs/releases.md b/docs/releases.md index 3563f387..87e58009 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -12,7 +12,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release --> -## ntfy server v1.25.1 +## ntfy server v1.25.2 Released June 2, 2022 This release adds the ability to set a log level to facilitate easier debugging of live systems. It also solves a @@ -25,7 +25,6 @@ more translations: Chinese/Simplified and Dutch. **Features:** * Advanced logging, with different log levels and hot reloading of the log level ([#284](https://github.com/binwiederhier/ntfy/pull/284)) -* Add `tzdata` to Docker image to allow overriding the timezone with `TZ` ([#307](https://github.com/binwiederhier/ntfy/pull/307), thanks to [@ksurl](https://github.com/ksurl)) **Bugs**: From 3ad5ed571df8b7e4d639b6c6b69ae3acbfe67bb4 Mon Sep 17 00:00:00 2001 From: Kazi Date: Sat, 4 Jun 2022 20:27:26 +0200 Subject: [PATCH 37/38] Scoop instructions --- docs/install.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/install.md b/docs/install.md index 210ca146..4190b064 100644 --- a/docs/install.md +++ b/docs/install.md @@ -211,6 +211,10 @@ 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). +Also available at [Scoop's](https://scoop.sh) Main repository: + +`scoop install ntfy` + !!! 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. From ffb6de7d9739aad451744d69116c794b85a0426f Mon Sep 17 00:00:00 2001 From: Kazi Date: Sat, 4 Jun 2022 20:29:21 +0200 Subject: [PATCH 38/38] fix typo --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 4190b064..f1fc3cf1 100644 --- a/docs/install.md +++ b/docs/install.md @@ -211,7 +211,7 @@ 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). -Also available at [Scoop's](https://scoop.sh) Main repository: +Also available in [Scoop's](https://scoop.sh) Main repository: `scoop install ntfy`