Compare commits
25 Commits
303-update
...
windows-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b0bd63af | ||
|
|
220372d65a | ||
|
|
353fedb93f | ||
|
|
dfd12528f3 | ||
|
|
6d5cc6aeac | ||
|
|
9f3883eaf0 | ||
|
|
a0ebd64461 | ||
|
|
dafd130fe5 | ||
|
|
11e9e1e6a0 | ||
|
|
b23f6632b1 | ||
|
|
6bacf7dafc | ||
|
|
0e200b96e0 | ||
|
|
3ce56879ae | ||
|
|
48efdffa57 | ||
|
|
9135bb277b | ||
|
|
711899ad35 | ||
|
|
01435d5fea | ||
|
|
8ce2188b28 | ||
|
|
a712d78e4c | ||
|
|
c0a5a1fb35 | ||
|
|
1c32ee7613 | ||
|
|
f356309f70 | ||
|
|
39936a95f8 | ||
|
|
16900d2c10 | ||
|
|
950ba1e2e1 |
@@ -48,13 +48,15 @@ builds:
|
|||||||
- id: ntfy_windows_amd64
|
- id: ntfy_windows_amd64
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
- CGO_ENABLED=1 # required for go-sqlite3
|
||||||
tags: [ noserver ] # don't include server files
|
- CC=x86_64-w64-mingw32-gcc # apt install gcc-mingw-w64-x86-64
|
||||||
|
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
|
||||||
ldflags:
|
ldflags:
|
||||||
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
- "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
|
||||||
goos: [ windows ]
|
goos: [ windows ]
|
||||||
goarch: [ amd64 ]
|
goarch: [amd64 ]
|
||||||
- id: ntfy_darwin_all
|
-
|
||||||
|
id: ntfy_darwin_all
|
||||||
binary: ntfy
|
binary: ntfy
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -31,6 +31,7 @@ help:
|
|||||||
@echo "Build server & client (without GoReleaser):"
|
@echo "Build server & client (without GoReleaser):"
|
||||||
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
@echo " make cli-linux-server - Build client & server (no GoReleaser, current arch, Linux)"
|
||||||
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
|
||||||
|
@echo " make cli-windows-server - Build client & server (no GoReleaser, amd64 only, Windows)"
|
||||||
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
|
||||||
@echo
|
@echo
|
||||||
@echo "Build dev Docker:"
|
@echo "Build dev Docker:"
|
||||||
@@ -106,6 +107,7 @@ build-deps-ubuntu:
|
|||||||
curl \
|
curl \
|
||||||
gcc-aarch64-linux-gnu \
|
gcc-aarch64-linux-gnu \
|
||||||
gcc-arm-linux-gnueabi \
|
gcc-arm-linux-gnueabi \
|
||||||
|
gcc-mingw-w64-x86-64 \
|
||||||
python3 \
|
python3 \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
jq
|
jq
|
||||||
@@ -201,6 +203,16 @@ cli-darwin-server: cli-deps-static-sites
|
|||||||
-ldflags \
|
-ldflags \
|
||||||
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
"-linkmode=external -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
|
cli-windows-server: cli-deps-static-sites
|
||||||
|
# This is a target to build the CLI (including the server) for Windows.
|
||||||
|
# Use this for Windows development, if you really don't want to install GoReleaser ...
|
||||||
|
mkdir -p dist/ntfy_windows_server server/docs
|
||||||
|
CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build \
|
||||||
|
-o dist/ntfy_windows_server/ntfy.exe \
|
||||||
|
-tags sqlite_omit_load_extension,osusergo,netgo \
|
||||||
|
-ldflags \
|
||||||
|
"-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(shell date +%s)"
|
||||||
|
|
||||||
cli-client: cli-deps-static-sites
|
cli-client: cli-deps-static-sites
|
||||||
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
# This is a target to build the CLI (excluding the server) manually. This should work on Linux/macOS/Windows.
|
||||||
# Use this for development, if you really don't want to install GoReleaser ...
|
# Use this for development, if you really don't want to install GoReleaser ...
|
||||||
@@ -213,7 +225,7 @@ cli-client: cli-deps-static-sites
|
|||||||
|
|
||||||
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
cli-deps: cli-deps-static-sites cli-deps-all cli-deps-gcc
|
||||||
|
|
||||||
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64
|
cli-deps-gcc: cli-deps-gcc-armv6-armv7 cli-deps-gcc-arm64 cli-deps-gcc-windows
|
||||||
|
|
||||||
cli-deps-static-sites:
|
cli-deps-static-sites:
|
||||||
mkdir -p server/docs server/site
|
mkdir -p server/docs server/site
|
||||||
@@ -228,6 +240,9 @@ cli-deps-gcc-armv6-armv7:
|
|||||||
cli-deps-gcc-arm64:
|
cli-deps-gcc-arm64:
|
||||||
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
||||||
|
|
||||||
|
cli-deps-gcc-windows:
|
||||||
|
which x86_64-w64-mingw32-gcc || { echo "ERROR: Windows cross compiler not installed. On Ubuntu, run: apt install gcc-mingw-w64-x86-64"; exit 1; }
|
||||||
|
|
||||||
cli-deps-update:
|
cli-deps-update:
|
||||||
go get -u
|
go get -u
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const (
|
|||||||
DefaultBaseURL = "https://ntfy.sh"
|
DefaultBaseURL = "https://ntfy.sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultConfigFile is the default path to the client config file (set in config_*.go)
|
||||||
|
var DefaultConfigFile string
|
||||||
|
|
||||||
// Config is the config struct for a Client
|
// Config is the config struct for a Client
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultHost string `yaml:"default-host"`
|
DefaultHost string `yaml:"default-host"`
|
||||||
|
|||||||
18
client/config_darwin.go
Normal file
18
client/config_darwin.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err == nil && u.Uid == "0" {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||||
|
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
18
client/config_unix.go
Normal file
18
client/config_unix.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err == nil && u.Uid == "0" {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/client.yml"
|
||||||
|
} else if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
client/config_windows.go
Normal file
14
client/config_windows.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
|
DefaultConfigFile = filepath.Join(configDir, "ntfy", "client.yml")
|
||||||
|
}
|
||||||
|
}
|
||||||
62
cmd/serve.go
62
cmd/serve.go
@@ -10,10 +10,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"runtime"
|
||||||
"os/signal"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
@@ -77,6 +76,7 @@ var flagsServe = append(
|
|||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-call-format", Aliases: []string{"twilio_call_format"}, EnvVars: []string{"NTFY_TWILIO_CALL_FORMAT"}, Usage: "Twilio/TwiML format string for phone calls"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
||||||
@@ -187,6 +187,7 @@ func execServe(c *cli.Context) error {
|
|||||||
twilioAuthToken := c.String("twilio-auth-token")
|
twilioAuthToken := c.String("twilio-auth-token")
|
||||||
twilioPhoneNumber := c.String("twilio-phone-number")
|
twilioPhoneNumber := c.String("twilio-phone-number")
|
||||||
twilioVerifyService := c.String("twilio-verify-service")
|
twilioVerifyService := c.String("twilio-verify-service")
|
||||||
|
twilioCallFormat := c.String("twilio-call-format")
|
||||||
messageSizeLimitStr := c.String("message-size-limit")
|
messageSizeLimitStr := c.String("message-size-limit")
|
||||||
messageDelayLimitStr := c.String("message-delay-limit")
|
messageDelayLimitStr := c.String("message-delay-limit")
|
||||||
totalTopicLimit := c.Int("global-topic-limit")
|
totalTopicLimit := c.Int("global-topic-limit")
|
||||||
@@ -347,6 +348,8 @@ func execServe(c *cli.Context) error {
|
|||||||
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
|
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
|
||||||
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
|
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
|
||||||
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
|
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
|
||||||
|
} else if runtime.GOOS == "windows" && listenUnix != "" {
|
||||||
|
return errors.New("listen-unix is not supported on Windows")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
@@ -456,6 +459,13 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.TwilioAuthToken = twilioAuthToken
|
conf.TwilioAuthToken = twilioAuthToken
|
||||||
conf.TwilioPhoneNumber = twilioPhoneNumber
|
conf.TwilioPhoneNumber = twilioPhoneNumber
|
||||||
conf.TwilioVerifyService = twilioVerifyService
|
conf.TwilioVerifyService = twilioVerifyService
|
||||||
|
if twilioCallFormat != "" {
|
||||||
|
tmpl, err := template.New("twiml").Parse(twilioCallFormat)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse twilio-call-format template: %w", err)
|
||||||
|
}
|
||||||
|
conf.TwilioCallFormat = tmpl
|
||||||
|
}
|
||||||
conf.MessageSizeLimit = int(messageSizeLimit)
|
conf.MessageSizeLimit = int(messageSizeLimit)
|
||||||
conf.MessageDelayMax = messageDelayLimit
|
conf.MessageDelayMax = messageDelayLimit
|
||||||
conf.TotalTopicLimit = totalTopicLimit
|
conf.TotalTopicLimit = totalTopicLimit
|
||||||
@@ -493,6 +503,14 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
||||||
conf.Version = c.App.Version
|
conf.Version = c.App.Version
|
||||||
|
|
||||||
|
// Check if we should run as a Windows service
|
||||||
|
if ranAsService, err := maybeRunAsService(conf); err != nil {
|
||||||
|
log.Fatal("%s", err.Error())
|
||||||
|
} else if ranAsService {
|
||||||
|
log.Info("Exiting.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Set up hot-reloading of config
|
// Set up hot-reloading of config
|
||||||
go sigHandlerConfigReload(config)
|
go sigHandlerConfigReload(config)
|
||||||
|
|
||||||
@@ -507,22 +525,6 @@ func execServe(c *cli.Context) error {
|
|||||||
return nil
|
return 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
|
|
||||||
}
|
|
||||||
if err := reloadLogLevel(inputSource); err != nil {
|
|
||||||
log.Warn("Reloading log level failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
||||||
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
|
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
|
||||||
prefix, err := netip.ParsePrefix(host)
|
prefix, err := netip.ParsePrefix(host)
|
||||||
@@ -653,25 +655,3 @@ func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Tok
|
|||||||
}
|
}
|
||||||
return tokens, nil
|
return tokens, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
|
||||||
newLevelStr, err := inputSource.String("log-level")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level: %s", err.Error())
|
|
||||||
}
|
|
||||||
overrides, err := inputSource.StringSlice("log-level-overrides")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level overrides (1): %s", err.Error())
|
|
||||||
}
|
|
||||||
log.ResetLevelOverrides()
|
|
||||||
if err := applyLogLevelOverrides(overrides); err != nil {
|
|
||||||
return fmt.Errorf("cannot load log level overrides (2): %s", err.Error())
|
|
||||||
}
|
|
||||||
log.SetLevel(log.ToLevel(newLevelStr))
|
|
||||||
if len(overrides) > 0 {
|
|
||||||
log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides))
|
|
||||||
} else {
|
|
||||||
log.Info("Log level is %v", strings.ToUpper(newLevelStr))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
55
cmd/serve_unix.go
Normal file
55
cmd/serve_unix.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
//go:build linux || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/v2/log"
|
||||||
|
"heckel.io/ntfy/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := reloadLogLevel(inputSource); err != nil {
|
||||||
|
log.Warn("Reloading log level failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
||||||
|
newLevelStr, err := inputSource.String("log-level")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
overrides, err := inputSource.StringSlice("log-level-overrides")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.ResetLevelOverrides()
|
||||||
|
if err := applyLogLevelOverrides(overrides); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.SetLevel(log.ToLevel(newLevelStr))
|
||||||
|
if len(overrides) > 0 {
|
||||||
|
log.Info("Log level is %v, %d override(s) in place", newLevelStr, len(overrides))
|
||||||
|
} else {
|
||||||
|
log.Info("Log level is %v", newLevelStr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
100
cmd/serve_windows.go
Normal file
100
cmd/serve_windows.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//go:build windows && !noserver
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"heckel.io/ntfy/v2/log"
|
||||||
|
"heckel.io/ntfy/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
const serviceName = "ntfy"
|
||||||
|
|
||||||
|
// sigHandlerConfigReload is a no-op on Windows since SIGHUP is not available.
|
||||||
|
// Windows users can restart the service to reload configuration.
|
||||||
|
func sigHandlerConfigReload(config string) {
|
||||||
|
log.Debug("Config hot-reload via SIGHUP is not supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runAsWindowsService runs the ntfy server as a Windows service
|
||||||
|
func runAsWindowsService(conf *server.Config) error {
|
||||||
|
return svc.Run(serviceName, &windowsService{conf: conf})
|
||||||
|
}
|
||||||
|
|
||||||
|
// windowsService implements the svc.Handler interface
|
||||||
|
type windowsService struct {
|
||||||
|
conf *server.Config
|
||||||
|
server *server.Server
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute is the main entry point for the Windows service
|
||||||
|
func (s *windowsService) Execute(args []string, requests <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||||
|
status <- svc.Status{State: svc.StartPending}
|
||||||
|
|
||||||
|
// Create and start the server
|
||||||
|
var err error
|
||||||
|
s.mu.Lock()
|
||||||
|
s.server, err = server.New(s.conf)
|
||||||
|
s.mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Failed to create server: %s", err.Error())
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server in a goroutine
|
||||||
|
serverErrChan := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
serverErrChan <- s.server.Run()
|
||||||
|
}()
|
||||||
|
|
||||||
|
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
log.Info("Windows service started")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-serverErrChan:
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Server error: %s", err.Error())
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
case req := <-requests:
|
||||||
|
switch req.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
status <- req.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
log.Info("Windows service stopping...")
|
||||||
|
status <- svc.Status{State: svc.StopPending}
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.server != nil {
|
||||||
|
s.server.Stop()
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
return false, 0
|
||||||
|
default:
|
||||||
|
log.Warn("Unexpected service control request: %d", req.Cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeRunAsService checks if the process is running as a Windows service,
|
||||||
|
// and if so, runs the server as a service. Returns true if it ran as a service.
|
||||||
|
func maybeRunAsService(conf *server.Config) (bool, error) {
|
||||||
|
isService, err := svc.IsWindowsService()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to detect Windows service mode: %w", err)
|
||||||
|
} else if !isService {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
log.Info("Running as Windows service")
|
||||||
|
if err := runAsWindowsService(conf); err != nil {
|
||||||
|
return true, fmt.Errorf("failed to run as Windows service: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
@@ -3,28 +3,21 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/v2/client"
|
"heckel.io/ntfy/v2/client"
|
||||||
"heckel.io/ntfy/v2/log"
|
"heckel.io/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/v2/util"
|
"heckel.io/ntfy/v2/util"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
commands = append(commands, cmdSubscribe)
|
commands = append(commands, cmdSubscribe)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
clientRootConfigFileUnixAbsolute = "/etc/ntfy/client.yml"
|
|
||||||
clientUserConfigFileUnixRelative = "ntfy/client.yml"
|
|
||||||
clientUserConfigFileWindowsRelative = "ntfy\\client.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var flagsSubscribe = append(
|
var flagsSubscribe = append(
|
||||||
append([]cli.Flag{}, flagsDefault...),
|
append([]cli.Flag{}, flagsDefault...),
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
||||||
@@ -310,45 +303,16 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
|
|||||||
if filename != "" {
|
if filename != "" {
|
||||||
return client.LoadConfig(filename)
|
return client.LoadConfig(filename)
|
||||||
}
|
}
|
||||||
configFile, err := defaultClientConfigFile()
|
if client.DefaultConfigFile != "" {
|
||||||
if err != nil {
|
if s, _ := os.Stat(client.DefaultConfigFile); s != nil {
|
||||||
log.Warn("Could not determine default client config file: %s", err.Error())
|
return client.LoadConfig(client.DefaultConfigFile)
|
||||||
} else {
|
|
||||||
if s, _ := os.Stat(configFile); s != nil {
|
|
||||||
return client.LoadConfig(configFile)
|
|
||||||
}
|
}
|
||||||
log.Debug("Config file %s not found", configFile)
|
log.Debug("Config file %s not found", client.DefaultConfigFile)
|
||||||
}
|
}
|
||||||
log.Debug("Loading default config")
|
log.Debug("Loading default config")
|
||||||
return client.NewConfig(), nil
|
return client.NewConfig(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//lint:ignore U1000 Conditionally used in different builds
|
|
||||||
func defaultClientConfigFileUnix() (string, error) {
|
|
||||||
u, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine current user: %w", err)
|
|
||||||
}
|
|
||||||
configFile := clientRootConfigFileUnixAbsolute
|
|
||||||
if u.Uid != "0" {
|
|
||||||
homeDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine user config dir: %w", err)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil
|
|
||||||
}
|
|
||||||
return configFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//lint:ignore U1000 Conditionally used in different builds
|
|
||||||
func defaultClientConfigFileWindows() (string, error) {
|
|
||||||
homeDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("could not determine user config dir: %w", err)
|
|
||||||
}
|
|
||||||
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func logMessagePrefix(m *client.Message) string {
|
func logMessagePrefix(m *client.Message) string {
|
||||||
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
|
return fmt.Sprintf("%s/%s", util.ShortTopicURL(m.TopicURL), m.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -10,7 +12,3 @@ or "~/Library/Application Support/ntfy/client.yml" for all other users.`
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"sh", "-c"}
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileUnix()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,3 @@ or ~/.config/ntfy/client.yml for all other users.`
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"sh", "-c"}
|
scriptLauncher = []string{"sh", "-c"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileUnix()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -9,7 +11,3 @@ const (
|
|||||||
var (
|
var (
|
||||||
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultClientConfigFile() (string, error) {
|
|
||||||
return defaultClientConfigFileWindows()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1261,10 +1261,85 @@ are the easiest), and then configure the following options:
|
|||||||
* `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
|
* `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
|
||||||
* `twilio-phone-number` is the outgoing phone number you purchased, e.g. +18775132586
|
* `twilio-phone-number` is the outgoing phone number you purchased, e.g. +18775132586
|
||||||
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
|
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
|
||||||
|
* `twilio-call-format` is the custom Twilio markup ([TwiML](https://www.twilio.com/docs/voice/twiml)) to use for phone calls (optional)
|
||||||
|
|
||||||
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
|
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
|
||||||
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
|
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
|
||||||
|
|
||||||
|
To customize the message that is spoken out loud, set the `twilio-call-format` option with [TwiML](https://www.twilio.com/docs/voice/twiml). The format is
|
||||||
|
rendered as a [Go template](https://pkg.go.dev/text/template), so you can use the following fields from the message:
|
||||||
|
|
||||||
|
* `{{.Topic}}` is the topic name
|
||||||
|
* `{{.Message}}` is the message body
|
||||||
|
* `{{.Title}}` is the message title
|
||||||
|
* `{{.Tags}}` is a list of tags
|
||||||
|
* `{{.Priority}}` is the message priority
|
||||||
|
* `{{.Sender}}` is the IP address or username of the sender
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
=== "Custom TwiML (English)"
|
||||||
|
``` yaml
|
||||||
|
twilio-account: "AC12345beefbeef67890beefbeef122586"
|
||||||
|
twilio-auth-token: "affebeef258625862586258625862586"
|
||||||
|
twilio-phone-number: "+18775132586"
|
||||||
|
twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
|
||||||
|
twilio-call-format: |
|
||||||
|
<Response>
|
||||||
|
<Pause length="1"/>
|
||||||
|
<Say loop="3">
|
||||||
|
Yo yo yo, you should totally check out this message for {{.Topic}}.
|
||||||
|
{{ if eq .Priority 5 }}
|
||||||
|
It's really really important, dude. So listen up!
|
||||||
|
{{ end }}
|
||||||
|
<break time="1s"/>
|
||||||
|
{{ if neq .Title "" }}
|
||||||
|
Bro, it's titled: {{.Title}}.
|
||||||
|
{{ end }}
|
||||||
|
<break time="1s"/>
|
||||||
|
{{.Message}}
|
||||||
|
<break time="1s"/>
|
||||||
|
That is all.
|
||||||
|
<break time="1s"/>
|
||||||
|
You know who this message is from? It is from {{.Sender}}.
|
||||||
|
<break time="3s"/>
|
||||||
|
</Say>
|
||||||
|
<Say>See ya!</Say>
|
||||||
|
</Response>
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Custom TwiML (German)"
|
||||||
|
``` yaml
|
||||||
|
twilio-account: "AC12345beefbeef67890beefbeef122586"
|
||||||
|
twilio-auth-token: "affebeef258625862586258625862586"
|
||||||
|
twilio-phone-number: "+18775132586"
|
||||||
|
twilio-verify-service: "VA12345beefbeef67890beefbeef122586"
|
||||||
|
twilio-call-format: |
|
||||||
|
<Response>
|
||||||
|
<Pause length="1"/>
|
||||||
|
<Say loop="3" voice="alice" language="de-DE">
|
||||||
|
Du hast eine Nachricht zum Thema {{.Topic}}.
|
||||||
|
{{ if eq .Priority 5 }}
|
||||||
|
Achtung. Die Nachricht ist sehr wichtig.
|
||||||
|
{{ end }}
|
||||||
|
<break time="1s"/>
|
||||||
|
{{ if neq .Title "" }}
|
||||||
|
Titel der Nachricht: {{.Title}}.
|
||||||
|
{{ end }}
|
||||||
|
<break time="1s"/>
|
||||||
|
Nachricht:
|
||||||
|
<break time="1s"/>
|
||||||
|
{{.Message}}
|
||||||
|
<break time="1s"/>
|
||||||
|
Ende der Nachricht.
|
||||||
|
<break time="1s"/>
|
||||||
|
Diese Nachricht wurde vom Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
|
||||||
|
<break time="3s"/>
|
||||||
|
</Say>
|
||||||
|
<Say voice="alice" language="de-DE">Alla mol!</Say>
|
||||||
|
</Response>
|
||||||
|
```
|
||||||
|
|
||||||
## Message limits
|
## Message limits
|
||||||
There are a few message limits that you can configure:
|
There are a few message limits that you can configure:
|
||||||
|
|
||||||
|
|||||||
@@ -228,19 +228,29 @@ brew install ntfy
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
|
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
||||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
To install, you can either
|
||||||
|
|
||||||
|
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
||||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
|
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
||||||
|
|
||||||
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
|
Once installed, you can run the ntfy CLI commands like so:
|
||||||
|
|
||||||
Also available in [Scoop's](https://scoop.sh) Main repository:
|
```
|
||||||
|
ntfy.exe -h
|
||||||
|
```
|
||||||
|
|
||||||
`scoop install ntfy`
|
The default configuration file location on Windows is `%ProgramData%\ntfy\server.yml` (e.g., `C:\ProgramData\ntfy\server.yml`)
|
||||||
|
for the server, and `%AppData%\ntfy\client.yml` for the client. You may need to create the directory and config file manually.
|
||||||
|
|
||||||
!!! info
|
To install the ntfy server as a Windows service, you can use the built-in `sc` command. For example, run this in an
|
||||||
There is currently no installer for Windows, and the binary is not signed. If this is desired, please create a
|
elevated command prompt (adjust the path to `ntfy.exe` accordingly):
|
||||||
[GitHub issue](https://github.com/binwiederhier/ntfy/issues) to let me know.
|
|
||||||
|
```
|
||||||
|
sc create ntfy binPath="C:\path\to\ntfy.exe serve" start=auto
|
||||||
|
sc start ntfy
|
||||||
|
```
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||||||
- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications
|
- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications
|
||||||
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
||||||
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
|
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
|
||||||
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
|
- [ntfysh-windows](https://github.com/mshafer1/ntfysh-windows) - A ntfy client for Windows Desktop
|
||||||
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
|
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
|
||||||
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
|
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
|
||||||
- [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11
|
- [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11
|
||||||
|
|||||||
@@ -1603,10 +1603,10 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications)
|
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||||
([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
||||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8)
|
* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
||||||
for the initial implementation)
|
* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328), thanks to [@wtf911](https://github.com/wtf911))
|
||||||
|
|
||||||
### ntfy Android app v1.22.x (UNRELEASED)
|
### ntfy Android app v1.22.x (UNRELEASED)
|
||||||
|
|
||||||
|
|||||||
@@ -129,3 +129,15 @@ keyboard.
|
|||||||
|
|
||||||
## iOS app
|
## iOS app
|
||||||
Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode.
|
Sorry, there is no way to debug or get the logs from the iOS app (yet), outside of running the app in Xcode.
|
||||||
|
|
||||||
|
## Other
|
||||||
|
|
||||||
|
### "Reconnecting..." / Late notifications on mobile (self-hosted)
|
||||||
|
|
||||||
|
If all of your topics are showing as "Reconnecting" and notifications are taking a long time (30+ minutes) to come in, or if you're only getting new pushes with a manual refresh, double-check your configuration:
|
||||||
|
|
||||||
|
* If ntfy is behind a reverse proxy (such as Nginx):
|
||||||
|
* Make sure `behind-proxy` is enabled in ntfy's config.
|
||||||
|
* Make sure WebSockets are enabled in the reverse proxy config.
|
||||||
|
* Make sure you have granted permission to access all of your topics, either to a logged-in user account or to `everyone`. All subscribed topics are joined into a single WebSocket/JSON request, so a single topic that receives `403 Forbidden` will prevent the entire request from going through.
|
||||||
|
* In particular, double-check that `everyone` has permission to write to `up*` and your user has permission to read `up*` if you are using UnifiedPush.
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -5,8 +5,8 @@ go 1.24.0
|
|||||||
toolchain go1.24.5
|
toolchain go1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.20.0 // indirect
|
cloud.google.com/go/firestore v1.21.0 // indirect
|
||||||
cloud.google.com/go/storage v1.59.0 // indirect
|
cloud.google.com/go/storage v1.59.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/emersion/go-smtp v0.18.0
|
github.com/emersion/go-smtp v0.18.0
|
||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.39.0
|
golang.org/x/term v0.39.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
google.golang.org/api v0.259.0
|
google.golang.org/api v0.260.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ require (
|
|||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0
|
github.com/stripe/stripe-go/v74 v74.30.0
|
||||||
|
golang.org/x/sys v0.40.0
|
||||||
golang.org/x/text v0.33.0
|
golang.org/x/text v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ require (
|
|||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
@@ -93,11 +94,10 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.49.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
|
||||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.78.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
28
go.sum
28
go.sum
@@ -8,8 +8,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo=
|
cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
|
||||||
cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo=
|
cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
|
||||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||||
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||||
@@ -18,8 +18,8 @@ cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7
|
|||||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||||
cloud.google.com/go/storage v1.59.0 h1:9p3yDzEN9Vet4JnbN90FECIw6n4FCXcKBK1scxtQnw8=
|
cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58=
|
||||||
cloud.google.com/go/storage v1.59.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
|
||||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||||
firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw=
|
firebase.google.com/go/v4 v4.18.0 h1:S+g0P72oDGqOaG4wlLErX3zQmU9plVdu7j+Bc3R1qFw=
|
||||||
@@ -96,8 +96,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
@@ -263,16 +263,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4=
|
||||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o=
|
||||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9 h1:wFALHMUiWKkK/x6rSxm79KpSnUyh7ks2E+mel670Dc4=
|
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3 h1:rUamZFBwsWVWg4Yb7iTbwYp81XVHUvOXNdrFCoYRRNE=
|
||||||
google.golang.org/genproto v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:wE6SUYr3iNtF/D0GxVAjT+0CbDFktQNssYs9PVptCt4=
|
google.golang.org/genproto v0.0.0-20260114163908-3f89685c29c3/go.mod h1:wE6SUYr3iNtF/D0GxVAjT+0CbDFktQNssYs9PVptCt4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9 h1:4DKBrmaqeptdEzp21EfrOEh8LE7PJ5ywH6wydSbOfGY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9 h1:IY6/YYRrFUk0JPp0xOVctvFIVuRnjccihY5kxf5g0TE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260112192933-99fd39fd28a9/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"heckel.io/ntfy/v2/user"
|
"heckel.io/ntfy/v2/user"
|
||||||
@@ -11,8 +12,6 @@ import (
|
|||||||
// Defines default config settings (excluding limits, see below)
|
// Defines default config settings (excluding limits, see below)
|
||||||
const (
|
const (
|
||||||
DefaultListenHTTP = ":80"
|
DefaultListenHTTP = ":80"
|
||||||
DefaultConfigFile = "/etc/ntfy/server.yml"
|
|
||||||
DefaultTemplateDir = "/etc/ntfy/templates"
|
|
||||||
DefaultCacheDuration = 12 * time.Hour
|
DefaultCacheDuration = 12 * time.Hour
|
||||||
DefaultCacheBatchTimeout = time.Duration(0)
|
DefaultCacheBatchTimeout = time.Duration(0)
|
||||||
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
||||||
@@ -26,6 +25,12 @@ const (
|
|||||||
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Platform-specific default paths (set in config_unix.go or config_windows.go)
|
||||||
|
var (
|
||||||
|
DefaultConfigFile string
|
||||||
|
DefaultTemplateDir string
|
||||||
|
)
|
||||||
|
|
||||||
// Defines default Web Push settings
|
// Defines default Web Push settings
|
||||||
const (
|
const (
|
||||||
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
|
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
|
||||||
@@ -128,6 +133,7 @@ type Config struct {
|
|||||||
TwilioCallsBaseURL string
|
TwilioCallsBaseURL string
|
||||||
TwilioVerifyBaseURL string
|
TwilioVerifyBaseURL string
|
||||||
TwilioVerifyService string
|
TwilioVerifyService string
|
||||||
|
TwilioCallFormat *template.Template
|
||||||
MetricsEnable bool
|
MetricsEnable bool
|
||||||
MetricsListenHTTP string
|
MetricsListenHTTP string
|
||||||
ProfileListenHTTP string
|
ProfileListenHTTP string
|
||||||
@@ -226,6 +232,7 @@ func NewConfig() *Config {
|
|||||||
TwilioPhoneNumber: "",
|
TwilioPhoneNumber: "",
|
||||||
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
|
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
|
||||||
TwilioVerifyService: "",
|
TwilioVerifyService: "",
|
||||||
|
TwilioCallFormat: nil,
|
||||||
MessageSizeLimit: DefaultMessageSizeLimit,
|
MessageSizeLimit: DefaultMessageSizeLimit,
|
||||||
MessageDelayMin: DefaultMessageDelayMin,
|
MessageDelayMin: DefaultMessageDelayMin,
|
||||||
MessageDelayMax: DefaultMessageDelayMax,
|
MessageDelayMax: DefaultMessageDelayMax,
|
||||||
|
|||||||
8
server/config_unix.go
Normal file
8
server/config_unix.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DefaultConfigFile = "/etc/ntfy/server.yml"
|
||||||
|
DefaultTemplateDir = "/etc/ntfy/templates"
|
||||||
|
}
|
||||||
17
server/config_windows.go
Normal file
17
server/config_windows.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
programData := os.Getenv("ProgramData")
|
||||||
|
if programData == "" {
|
||||||
|
programData = `C:\ProgramData`
|
||||||
|
}
|
||||||
|
DefaultConfigFile = filepath.Join(programData, "ntfy", "server.yml")
|
||||||
|
DefaultTemplateDir = filepath.Join(programData, "ntfy", "templates")
|
||||||
|
}
|
||||||
@@ -216,11 +216,13 @@
|
|||||||
# - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
|
# - twilio-auth-token is the Twilio auth token, e.g. affebeef258625862586258625862586
|
||||||
# - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
|
# - twilio-phone-number is the outgoing phone number you purchased, e.g. +18775132586
|
||||||
# - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
|
# - twilio-verify-service is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
|
||||||
|
# - twilio-call-format is the custom TwiML send to the Call API (optional, see https://www.twilio.com/docs/voice/twiml)
|
||||||
#
|
#
|
||||||
# twilio-account:
|
# twilio-account:
|
||||||
# twilio-auth-token:
|
# twilio-auth-token:
|
||||||
# twilio-phone-number:
|
# twilio-phone-number:
|
||||||
# twilio-verify-service:
|
# twilio-verify-service:
|
||||||
|
# twilio-call-format:
|
||||||
|
|
||||||
# Interval in which keepalive messages are sent to the client. This is to prevent
|
# Interval in which keepalive messages are sent to the client. This is to prevent
|
||||||
# intermediaries closing the connection for inactivity.
|
# intermediaries closing the connection for inactivity.
|
||||||
|
|||||||
@@ -4,33 +4,49 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/v2/log"
|
|
||||||
"heckel.io/ntfy/v2/user"
|
|
||||||
"heckel.io/ntfy/v2/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"heckel.io/ntfy/v2/log"
|
||||||
|
"heckel.io/ntfy/v2/user"
|
||||||
|
"heckel.io/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// defaultTwilioCallFormatTemplate is the default TwiML template used for Twilio calls.
|
||||||
twilioCallFormat = `
|
// It can be overridden in the server configuration's twilio-call-format field.
|
||||||
|
//
|
||||||
|
// The format uses Go template syntax with the following fields:
|
||||||
|
// {{.Topic}}, {{.Title}}, {{.Message}}, {{.Priority}}, {{.Tags}}, {{.Sender}}
|
||||||
|
// String fields are automatically XML-escaped.
|
||||||
|
var defaultTwilioCallFormatTemplate = template.Must(template.New("twiml").Parse(`
|
||||||
<Response>
|
<Response>
|
||||||
<Pause length="1"/>
|
<Pause length="1"/>
|
||||||
<Say loop="3">
|
<Say loop="3">
|
||||||
You have a message from notify on topic %s. Message:
|
You have a message from notify on topic {{.Topic}}. Message:
|
||||||
<break time="1s"/>
|
<break time="1s"/>
|
||||||
%s
|
{{.Message}}
|
||||||
<break time="1s"/>
|
<break time="1s"/>
|
||||||
End of message.
|
End of message.
|
||||||
<break time="1s"/>
|
<break time="1s"/>
|
||||||
This message was sent by user %s. It will be repeated three times.
|
This message was sent by user {{.Sender}}. It will be repeated three times.
|
||||||
To unsubscribe from calls like this, remove your phone number in the notify web app.
|
To unsubscribe from calls like this, remove your phone number in the notify web app.
|
||||||
<break time="3s"/>
|
<break time="3s"/>
|
||||||
</Say>
|
</Say>
|
||||||
<Say>Goodbye.</Say>
|
<Say>Goodbye.</Say>
|
||||||
</Response>`
|
</Response>`))
|
||||||
)
|
|
||||||
|
// twilioCallData holds the data passed to the Twilio call format template
|
||||||
|
type twilioCallData struct {
|
||||||
|
Topic string
|
||||||
|
Title string
|
||||||
|
Message string
|
||||||
|
Priority int
|
||||||
|
Tags []string
|
||||||
|
Sender string
|
||||||
|
}
|
||||||
|
|
||||||
// convertPhoneNumber checks if the given phone number is verified for the given user, and if so, returns the verified
|
// convertPhoneNumber checks if the given phone number is verified for the given user, and if so, returns the verified
|
||||||
// phone number. It also converts a boolean string ("yes", "1", "true") to the first verified phone number.
|
// phone number. It also converts a boolean string ("yes", "1", "true") to the first verified phone number.
|
||||||
@@ -65,7 +81,29 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
|
|||||||
if u != nil {
|
if u != nil {
|
||||||
sender = u.Name
|
sender = u.Name
|
||||||
}
|
}
|
||||||
body := fmt.Sprintf(twilioCallFormat, xmlEscapeText(m.Topic), xmlEscapeText(m.Message), xmlEscapeText(sender))
|
tmpl := defaultTwilioCallFormatTemplate
|
||||||
|
if s.config.TwilioCallFormat != nil {
|
||||||
|
tmpl = s.config.TwilioCallFormat
|
||||||
|
}
|
||||||
|
tags := make([]string, len(m.Tags))
|
||||||
|
for i, tag := range m.Tags {
|
||||||
|
tags[i] = xmlEscapeText(tag)
|
||||||
|
}
|
||||||
|
templateData := &twilioCallData{
|
||||||
|
Topic: xmlEscapeText(m.Topic),
|
||||||
|
Title: xmlEscapeText(m.Title),
|
||||||
|
Message: xmlEscapeText(m.Message),
|
||||||
|
Priority: m.Priority,
|
||||||
|
Tags: tags,
|
||||||
|
Sender: xmlEscapeText(sender),
|
||||||
|
}
|
||||||
|
var bodyBuf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&bodyBuf, templateData); err != nil {
|
||||||
|
logvrm(v, r, m).Tag(tagTwilio).Err(err).Warn("Error executing Twilio call format template")
|
||||||
|
minc(metricCallsMadeFailure)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body := bodyBuf.String()
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("From", s.config.TwilioPhoneNumber)
|
data.Set("From", s.config.TwilioPhoneNumber)
|
||||||
data.Set("To", to)
|
data.Set("To", to)
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"heckel.io/ntfy/v2/user"
|
|
||||||
"heckel.io/ntfy/v2/util"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/v2/user"
|
||||||
|
"heckel.io/ntfy/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
|
func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
|
||||||
@@ -202,6 +204,67 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
|
||||||
|
var called atomic.Bool
|
||||||
|
twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if called.Load() {
|
||||||
|
t.Fatal("Should be only called once")
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
|
||||||
|
require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+language%3D%22de-DE%22+loop%3D%223%22%3E%0A%09%09Du+hast+eine+Nachricht+von+notify+im+Thema+mytopic.+Nachricht%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Ende+der+Nachricht.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Diese+Nachricht+wurde+von+Benutzer+phil+gesendet.+Sie+wird+drei+Mal+wiederholt.%0A%09%09Um+dich+von+Anrufen+wie+diesen+abzumelden%2C+entferne+deine+Telefonnummer+in+der+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay+language%3D%22de-DE%22%3EAuf+Wiederh%C3%B6ren.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
|
||||||
|
called.Store(true)
|
||||||
|
}))
|
||||||
|
defer twilioServer.Close()
|
||||||
|
|
||||||
|
c := newTestConfigWithAuthFile(t)
|
||||||
|
c.TwilioCallsBaseURL = twilioServer.URL
|
||||||
|
c.TwilioAccount = "AC1234567890"
|
||||||
|
c.TwilioAuthToken = "AAEAA1234567890"
|
||||||
|
c.TwilioPhoneNumber = "+1234567890"
|
||||||
|
c.TwilioCallFormat = template.Must(template.New("twiml").Parse(`
|
||||||
|
<Response>
|
||||||
|
<Pause length="1"/>
|
||||||
|
<Say language="de-DE" loop="3">
|
||||||
|
Du hast eine Nachricht von notify im Thema {{.Topic}}. Nachricht:
|
||||||
|
<break time="1s"/>
|
||||||
|
{{.Message}}
|
||||||
|
<break time="1s"/>
|
||||||
|
Ende der Nachricht.
|
||||||
|
<break time="1s"/>
|
||||||
|
Diese Nachricht wurde von Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
|
||||||
|
Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
|
||||||
|
<break time="3s"/>
|
||||||
|
</Say>
|
||||||
|
<Say language="de-DE">Auf Wiederhören.</Say>
|
||||||
|
</Response>`))
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Add tier and user
|
||||||
|
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||||
|
Code: "pro",
|
||||||
|
MessageLimit: 10,
|
||||||
|
CallLimit: 1,
|
||||||
|
}))
|
||||||
|
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||||
|
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||||
|
u, err := s.userManager.User("phil")
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
|
||||||
|
|
||||||
|
// Do the thing
|
||||||
|
response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
|
||||||
|
"authorization": util.BasicAuth("phil", "phil"),
|
||||||
|
"x-call": "+11122233344",
|
||||||
|
})
|
||||||
|
require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
|
||||||
|
waitFor(t, func() bool {
|
||||||
|
return called.Load()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
|
func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
|
||||||
c := newTestConfigWithAuthFile(t)
|
c := newTestConfigWithAuthFile(t)
|
||||||
c.TwilioCallsBaseURL = "http://dummy.invalid"
|
c.TwilioCallsBaseURL = "http://dummy.invalid"
|
||||||
|
|||||||
18
web/package-lock.json
generated
18
web/package-lock.json
generated
@@ -3702,9 +3702,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.14",
|
"version": "2.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
|
||||||
"integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
|
"integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -8436,9 +8436,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.44.1",
|
"version": "5.46.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
||||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9121,9 +9121,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/which-typed-array": {
|
"node_modules/which-typed-array": {
|
||||||
"version": "1.1.19",
|
"version": "1.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
|
||||||
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
|
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -403,5 +403,7 @@
|
|||||||
"prefs_appearance_theme_light": "Světlý režim",
|
"prefs_appearance_theme_light": "Světlý režim",
|
||||||
"web_push_subscription_expiring_title": "Oznámení budou pozastavena",
|
"web_push_subscription_expiring_title": "Oznámení budou pozastavena",
|
||||||
"web_push_unknown_notification_title": "Neznámé oznámení přijaté ze serveru",
|
"web_push_unknown_notification_title": "Neznámé oznámení přijaté ze serveru",
|
||||||
"web_push_unknown_notification_body": "Možná bude nutné aktualizovat ntfy otevřením webové aplikace"
|
"web_push_unknown_notification_body": "Možná bude nutné aktualizovat ntfy otevřením webové aplikace",
|
||||||
|
"account_basics_cannot_edit_or_delete_provisioned_user": "Přiděleného uživatele nelze upravovat ani odstranit",
|
||||||
|
"account_tokens_table_cannot_delete_or_edit_provisioned_token": "Nelze upravit ani odstranit přidělený token"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,10 @@
|
|||||||
"publish_dialog_progress_uploading": "Mengunggah …",
|
"publish_dialog_progress_uploading": "Mengunggah …",
|
||||||
"notifications_more_details": "Untuk informasi lanjut, lihat <websiteLink>situs web</websiteLink> atau <docsLink>dokumentasi</docsLink>.",
|
"notifications_more_details": "Untuk informasi lanjut, lihat <websiteLink>situs web</websiteLink> atau <docsLink>dokumentasi</docsLink>.",
|
||||||
"publish_dialog_progress_uploading_detail": "Mengunggah {{loaded}}/{{total}} ({{percent}}%) …",
|
"publish_dialog_progress_uploading_detail": "Mengunggah {{loaded}}/{{total}} ({{percent}}%) …",
|
||||||
"publish_dialog_message_published": "Notifikasi dipublikasi",
|
"publish_dialog_message_published": "Notifikasi dipublikasikan",
|
||||||
"notifications_loading": "Memuat notifikasi …",
|
"notifications_loading": "Memuat notifikasi …",
|
||||||
"publish_dialog_base_url_label": "URL Layanan",
|
"publish_dialog_base_url_label": "URL Layanan",
|
||||||
"publish_dialog_title_placeholder": "Judul notifikasi, mis. Peringatan ruang disk",
|
"publish_dialog_title_placeholder": "Judul notifikasi, contoh: Peringatan ruang penyimpanan disk",
|
||||||
"publish_dialog_tags_label": "Tanda",
|
"publish_dialog_tags_label": "Tanda",
|
||||||
"publish_dialog_priority_label": "Prioritas",
|
"publish_dialog_priority_label": "Prioritas",
|
||||||
"publish_dialog_base_url_placeholder": "URL Layanan, mis. https://contoh.com",
|
"publish_dialog_base_url_placeholder": "URL Layanan, mis. https://contoh.com",
|
||||||
@@ -73,10 +73,10 @@
|
|||||||
"publish_dialog_topic_label": "Nama topik",
|
"publish_dialog_topic_label": "Nama topik",
|
||||||
"publish_dialog_message_placeholder": "Tulis pesan di sini",
|
"publish_dialog_message_placeholder": "Tulis pesan di sini",
|
||||||
"publish_dialog_click_label": "Klik URL",
|
"publish_dialog_click_label": "Klik URL",
|
||||||
"publish_dialog_tags_placeholder": "Daftar label yang dipisah dengan tanda koma, contoh: peringatan, cadangan-srv1",
|
"publish_dialog_tags_placeholder": "Daftar label yang dipisahkan koma, contoh: peringatan, cadangan-srv1",
|
||||||
"publish_dialog_click_placeholder": "URL yang dibuka ketika notifikasi diklik",
|
"publish_dialog_click_placeholder": "URL yang dibuka ketika notifikasi diklik",
|
||||||
"publish_dialog_email_label": "Email",
|
"publish_dialog_email_label": "Email",
|
||||||
"publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, mis. andi@contoh.com",
|
"publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, contoh: phil@example.com",
|
||||||
"publish_dialog_attach_label": "URL Lampiran",
|
"publish_dialog_attach_label": "URL Lampiran",
|
||||||
"publish_dialog_filename_label": "Nama File",
|
"publish_dialog_filename_label": "Nama File",
|
||||||
"publish_dialog_filename_placeholder": "Nama file lampiran",
|
"publish_dialog_filename_placeholder": "Nama file lampiran",
|
||||||
|
|||||||
Reference in New Issue
Block a user