mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-23 17:02:52 +03:00
Merge branch 'main' into metrics
This commit is contained in:
commit
754b456320
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '17'
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '17'
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
name: Install node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '17'
|
||||
node-version: '18'
|
||||
-
|
||||
name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
@ -5,10 +5,12 @@
|
||||
#
|
||||
# default-host: https://ntfy.sh
|
||||
|
||||
# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
|
||||
# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
|
||||
# For an empty password, use empty double-quotes ("")
|
||||
#
|
||||
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
||||
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
||||
# use empty double-quotes ("")
|
||||
|
||||
# default-token:
|
||||
|
||||
# default-user:
|
||||
# default-password:
|
||||
|
||||
@ -30,6 +32,8 @@
|
||||
# command: 'notify-send "$m"'
|
||||
# user: phill
|
||||
# password: mypass
|
||||
# - topic: token_topic
|
||||
# token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
#
|
||||
# Variables:
|
||||
# Variable Aliases Description
|
||||
|
@ -15,14 +15,19 @@ type Config struct {
|
||||
DefaultHost string `yaml:"default-host"`
|
||||
DefaultUser string `yaml:"default-user"`
|
||||
DefaultPassword *string `yaml:"default-password"`
|
||||
DefaultToken string `yaml:"default-token"`
|
||||
DefaultCommand string `yaml:"default-command"`
|
||||
Subscribe []struct {
|
||||
Subscribe []Subscribe `yaml:"subscribe"`
|
||||
}
|
||||
|
||||
// Subscribe is the struct for a Subscription within Config
|
||||
type Subscribe struct {
|
||||
Topic string `yaml:"topic"`
|
||||
User string `yaml:"user"`
|
||||
Password *string `yaml:"password"`
|
||||
Token string `yaml:"token"`
|
||||
Command string `yaml:"command"`
|
||||
If map[string]string `yaml:"if"`
|
||||
} `yaml:"subscribe"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new Config struct for a Client
|
||||
@ -31,6 +36,7 @@ func NewConfig() *Config {
|
||||
DefaultHost: DefaultBaseURL,
|
||||
DefaultUser: "",
|
||||
DefaultPassword: nil,
|
||||
DefaultToken: "",
|
||||
DefaultCommand: "",
|
||||
Subscribe: nil,
|
||||
}
|
||||
|
@ -116,3 +116,25 @@ subscribe:
|
||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
}
|
||||
|
||||
func TestConfig_DefaultToken(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||
default-host: http://localhost
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
`), 0600))
|
||||
|
||||
conf, err := client.LoadConfig(filename)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||
require.Equal(t, "", conf.DefaultUser)
|
||||
require.Nil(t, conf.DefaultPassword)
|
||||
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
||||
require.Equal(t, 1, len(conf.Subscribe))
|
||||
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
||||
require.Equal(t, "", conf.Subscribe[0].User)
|
||||
require.Nil(t, conf.Subscribe[0].Password)
|
||||
require.Equal(t, "", conf.Subscribe[0].Token)
|
||||
}
|
||||
|
@ -154,8 +154,7 @@ func execPublish(c *cli.Context) error {
|
||||
}
|
||||
if token != "" {
|
||||
options = append(options, client.WithBearerAuth(token))
|
||||
}
|
||||
if user != "" {
|
||||
} else if user != "" {
|
||||
var pass string
|
||||
parts := strings.SplitN(user, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
@ -171,7 +170,9 @@ func execPublish(c *cli.Context) error {
|
||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||
}
|
||||
options = append(options, client.WithBasicAuth(user, pass))
|
||||
} else if token == "" && conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||
} else if conf.DefaultToken != "" {
|
||||
options = append(options, client.WithBearerAuth(conf.DefaultToken))
|
||||
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
||||
}
|
||||
if pid > 0 {
|
||||
|
@ -5,8 +5,11 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -130,7 +133,7 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
||||
|
||||
// Tests with NTFY_TOPIC set ////
|
||||
require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
|
||||
t.Setenv("NTFY_TOPIC", topic)
|
||||
|
||||
// Test: Successful command with NTFY_TOPIC
|
||||
app, _, stdout, _ = newTestApp()
|
||||
@ -147,3 +150,151 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||
m = toMessage(t, stdout.String())
|
||||
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: fakepass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
||||
m := toMessage(t, stdout.String())
|
||||
require.Equal(t, "triggered", m.Message)
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
|
||||
app, _, _, _ := newTestApp()
|
||||
err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "cannot set both --user and --token", err.Error())
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ var flagsSubscribe = append(
|
||||
&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"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||
&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
|
||||
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
|
||||
&cli.BoolFlag{Name: "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"},
|
||||
@ -97,11 +98,18 @@ func execSubscribe(c *cli.Context) error {
|
||||
cl := client.New(conf)
|
||||
since := c.String("since")
|
||||
user := c.String("user")
|
||||
token := c.String("token")
|
||||
poll := c.Bool("poll")
|
||||
scheduled := c.Bool("scheduled")
|
||||
fromConfig := c.Bool("from-config")
|
||||
topic := c.Args().Get(0)
|
||||
command := c.Args().Get(1)
|
||||
|
||||
// Checks
|
||||
if user != "" && token != "" {
|
||||
return errors.New("cannot set both --user and --token")
|
||||
}
|
||||
|
||||
if !fromConfig {
|
||||
conf.Subscribe = nil // wipe if --from-config not passed
|
||||
}
|
||||
@ -109,6 +117,9 @@ func execSubscribe(c *cli.Context) error {
|
||||
if since != "" {
|
||||
options = append(options, client.WithSince(since))
|
||||
}
|
||||
if token != "" {
|
||||
options = append(options, client.WithBearerAuth(token))
|
||||
}
|
||||
if user != "" {
|
||||
var pass string
|
||||
parts := strings.SplitN(user, ":", 2)
|
||||
@ -126,9 +137,6 @@ func execSubscribe(c *cli.Context) error {
|
||||
}
|
||||
options = append(options, client.WithBasicAuth(user, pass))
|
||||
}
|
||||
if poll {
|
||||
options = append(options, client.WithPoll())
|
||||
}
|
||||
if scheduled {
|
||||
options = append(options, client.WithScheduled())
|
||||
}
|
||||
@ -145,6 +153,9 @@ func execSubscribe(c *cli.Context) error {
|
||||
|
||||
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
||||
for _, s := range conf.Subscribe { // may be nil
|
||||
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
||||
options = append(options, auth)
|
||||
}
|
||||
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -175,21 +186,11 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
for filter, value := range s.If {
|
||||
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
||||
}
|
||||
var user string
|
||||
var password *string
|
||||
if s.User != "" {
|
||||
user = s.User
|
||||
} else if conf.DefaultUser != "" {
|
||||
user = conf.DefaultUser
|
||||
}
|
||||
if s.Password != nil {
|
||||
password = s.Password
|
||||
} else if conf.DefaultPassword != nil {
|
||||
password = conf.DefaultPassword
|
||||
}
|
||||
if user != "" && password != nil {
|
||||
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
|
||||
|
||||
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
||||
topicOptions = append(topicOptions, auth)
|
||||
}
|
||||
|
||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||
if s.Command != "" {
|
||||
cmds[subscriptionID] = s.Command
|
||||
@ -214,6 +215,25 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
||||
// check for subscription token then subscription user:pass
|
||||
if s.Token != "" {
|
||||
return client.WithBearerAuth(s.Token)
|
||||
}
|
||||
if s.User != "" && s.Password != nil {
|
||||
return client.WithBasicAuth(s.User, *s.Password)
|
||||
}
|
||||
|
||||
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
||||
if conf.DefaultToken != "" {
|
||||
return client.WithBearerAuth(conf.DefaultToken)
|
||||
}
|
||||
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
|
||||
if command != "" {
|
||||
runCommand(c, command, m)
|
||||
|
312
cmd/subscribe_test.go
Normal file
312
cmd/subscribe_test.go
Normal file
@ -0,0 +1,312 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
user: philipp
|
||||
password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: fake
|
||||
default-password: password
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
user: philipp
|
||||
password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-user: philipp
|
||||
default-password: mypass
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
user: philipp
|
||||
password: mypass
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_FAKETOKEN0123456789FAKETOKEN
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *testing.T) {
|
||||
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(message))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||
default-host: %s
|
||||
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||
subscribe:
|
||||
- topic: mytopic
|
||||
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||
`, server.URL)), 0600))
|
||||
|
||||
app, _, stdout, _ := newTestApp()
|
||||
|
||||
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
|
||||
|
||||
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||
}
|
||||
|
||||
func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
|
||||
app, _, _, _ := newTestApp()
|
||||
err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "cannot set both --user and --token", err.Error())
|
||||
}
|
6
docs/hooks.py
Normal file
6
docs/hooks.py
Normal file
@ -0,0 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def copy_fonts(config, **kwargs):
|
||||
site_dir = config['site_dir']
|
||||
shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
|
@ -16,6 +16,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
|
||||
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
|
||||
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
|
||||
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
|
||||
|
||||
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
|
||||
and uptime of third party servers, so use of each server is **at your own discretion**.
|
||||
@ -75,6 +76,7 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
|
||||
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
|
||||
- [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
|
||||
- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
|
||||
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
|
||||
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
|
||||
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
|
||||
@ -117,6 +119,7 @@ and uptime of third party servers, so use of each server is **at your own discre
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
|
||||
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
|
||||
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
|
||||
- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023
|
||||
|
@ -3177,10 +3177,11 @@ These limits can be changed on a per-user basis using [tiers](config.md#tiers).
|
||||
a higher tier. ntfy.sh offers multiple paid tiers, which allows for much hier limits than the ones listed above.
|
||||
|
||||
## List of all parameters
|
||||
The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**,
|
||||
and can be passed as **HTTP headers** or **query parameters in the URL**. They are listed in the table in their canonical form.
|
||||
The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**
|
||||
when used in **HTTP headers**, and must be **lowercase** when used as **query parameters in the URL**. They are listed in the
|
||||
table in their canonical form.
|
||||
|
||||
| Parameter | Aliases (case-insensitive) | Description |
|
||||
| Parameter | Aliases | Description |
|
||||
|-----------------|--------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||
| `X-Message` | `Message`, `m` | Main body of the message as shown in the notification |
|
||||
| `X-Title` | `Title`, `t` | [Message title](#message-title) |
|
||||
|
@ -2,6 +2,38 @@
|
||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||
|
||||
## ntfy server v2.2.0 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Docs: Removed dependency on Google Fonts in docs ([#554](https://github.com/binwiederhier/ntfy/issues/554), thanks to [@bt90](https://github.com/bt90) for reporting, and [@ozskywalker](https://github.com/ozskywalker) for implementing)
|
||||
* Increase allowed auth failure attempts per IP address to 30 (no ticket)
|
||||
* Web app: Increase maximum incremental backoff retry interval to 2 minutes (no ticket)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Make query parameter description more clear ([#630](https://github.com/binwiederhier/ntfy/issues/630), thanks to [@bbaa-bbaa](https://github.com/bbaa-bbaa) for reporting, and to [@wunter8](https://github.com/wunter8) for a fix)
|
||||
|
||||
## ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
|
||||
|
||||
**Additional languages:**
|
||||
|
||||
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/))
|
||||
|
||||
## ntfy server v2.1.2
|
||||
Released March 4, 2023
|
||||
|
||||
|
56
docs/static/css/extra.css
vendored
56
docs/static/css/extra.css
vendored
@ -3,6 +3,8 @@
|
||||
--md-primary-fg-color--light: #338574;
|
||||
--md-primary-fg-color--dark: #338574;
|
||||
--md-footer-bg-color: #353744;
|
||||
--md-text-font: "Roboto";
|
||||
--md-code-font: "Roboto Mono";
|
||||
}
|
||||
|
||||
.md-header__button.md-logo :is(img, svg) {
|
||||
@ -147,3 +149,57 @@ figure video {
|
||||
.lightbox .close-lightbox:hover::before {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* roboto-300 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('../fonts/roboto-v30-latin-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-italic - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/roboto-v30-latin-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-500 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('../fonts/roboto-v30-latin-500.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-700 - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('../fonts/roboto-v30-latin-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-mono - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
font-family: 'Roboto Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('../fonts/roboto-mono-v22-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
|
BIN
docs/static/fonts/roboto-mono-v22-latin-regular.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-mono-v22-latin-regular.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/roboto-v30-latin-300.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-v30-latin-300.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/roboto-v30-latin-500.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-v30-latin-500.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/roboto-v30-latin-700.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-v30-latin-700.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/roboto-v30-latin-italic.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-v30-latin-italic.woff2
vendored
Normal file
Binary file not shown.
BIN
docs/static/fonts/roboto-v30-latin-regular.woff2
vendored
Normal file
BIN
docs/static/fonts/roboto-v30-latin-regular.woff2
vendored
Normal file
Binary file not shown.
@ -254,13 +254,13 @@ I hope this shows how powerful this command is. Here's a short video that demons
|
||||
<figcaption>Execute all the things</figcaption>
|
||||
</figure>
|
||||
|
||||
If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
|
||||
`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
|
||||
override the defaults.
|
||||
If most (or all) of your subscriptions use the same credentials, you can set defaults in `client.yml`. Use `default-user` and `default-password` or `default-token` (but not both).
|
||||
You can also specify a `default-command` that will run when a message is received. If a subscription does not include credentials to use or does not have a command, the defaults
|
||||
will be used, otherwise, the subscription settings will override the defaults.
|
||||
|
||||
!!! warning
|
||||
Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
|
||||
be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||
Because the `default-user`, `default-password`, and `default-token` will be sent for each topic that does not have its own username/password (even if the topic does not
|
||||
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||
|
||||
### Using the systemd service
|
||||
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||
|
@ -9,6 +9,7 @@ edit_uri: blob/main/docs/
|
||||
|
||||
theme:
|
||||
name: material
|
||||
font: false
|
||||
language: en
|
||||
custom_dir: docs/_overrides
|
||||
logo: static/img/ntfy.png
|
||||
@ -70,6 +71,9 @@ plugins:
|
||||
- search
|
||||
- minify:
|
||||
minify_html: true
|
||||
- mkdocs-simple-hooks:
|
||||
hooks:
|
||||
on_post_build: "docs.hooks:copy_fonts"
|
||||
|
||||
nav:
|
||||
- "Getting started": index.md
|
||||
|
@ -1,3 +1,4 @@
|
||||
# The documentation uses 'mkdocs', which is written in Python
|
||||
mkdocs-material
|
||||
mkdocs-minify-plugin
|
||||
mkdocs-simple-hooks
|
||||
|
@ -49,7 +49,7 @@ const (
|
||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||
DefaultVisitorAccountCreationLimitBurst = 3
|
||||
DefaultVisitorAccountCreationLimitReplenish = 24 * time.Hour
|
||||
DefaultVisitorAuthFailureLimitBurst = 10
|
||||
DefaultVisitorAuthFailureLimitBurst = 30
|
||||
DefaultVisitorAuthFailureLimitReplenish = time.Minute
|
||||
DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB
|
||||
DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB
|
||||
|
@ -1642,6 +1642,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
|
||||
// maybeAuthenticate reads the "Authorization" header and will try to authenticate the user
|
||||
// if it is set.
|
||||
//
|
||||
// - If auth-file is not configured, immediately return an IP-based visitor
|
||||
// - If the header is not set or not supported (anything non-Basic and non-Bearer),
|
||||
// an IP-based visitor is returned
|
||||
// - If the header is set, authenticate will be called to check the username/password (Basic auth),
|
||||
@ -1653,13 +1654,14 @@ func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
|
||||
// Read "Authorization" header value, and exit out early if it's not set
|
||||
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||
vip := s.visitor(ip, nil)
|
||||
if s.userManager == nil {
|
||||
return vip, nil
|
||||
}
|
||||
header, err := readAuthHeader(r)
|
||||
if err != nil {
|
||||
return vip, err
|
||||
} else if !supportedAuthHeader(header) {
|
||||
return vip, nil
|
||||
} else if s.userManager == nil {
|
||||
return vip, errHTTPUnauthorized
|
||||
}
|
||||
// If we're trying to auth, check the rate limiter first
|
||||
if !vip.AuthAllowed() {
|
||||
|
@ -796,6 +796,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
|
||||
|
||||
func TestServer_Auth_Fail_Rate_Limiting(t *testing.T) {
|
||||
c := newTestConfigWithAuthFile(t)
|
||||
c.VisitorAuthFailureLimitBurst = 10
|
||||
s := newTestServer(t, c)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
|
@ -133,29 +133,6 @@ func TestManager_AddUser_And_Query(t *testing.T) {
|
||||
require.Equal(t, u.ID, u3.ID)
|
||||
}
|
||||
|
||||
func TestManager_Authenticate_Timing(t *testing.T) {
|
||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
||||
|
||||
// Timing a correct attempt
|
||||
start := time.Now().UnixMilli()
|
||||
_, err := a.Authenticate("user", "pass")
|
||||
require.Nil(t, err)
|
||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
||||
|
||||
// Timing an incorrect attempt
|
||||
start = time.Now().UnixMilli()
|
||||
_, err = a.Authenticate("user", "INCORRECT")
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
||||
|
||||
// Timing a non-existing user attempt
|
||||
start = time.Now().UnixMilli()
|
||||
_, err = a.Authenticate("DOES-NOT-EXIST", "hithere")
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
||||
}
|
||||
|
||||
func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
|
||||
|
@ -6,8 +6,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local(''),
|
||||
url('../fonts/roboto-v29-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-v29-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-v29-latin-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-regular - latin */
|
||||
@ -16,8 +15,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('../fonts/roboto-v29-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-v29-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-v29-latin-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-500 - latin */
|
||||
@ -26,8 +24,7 @@
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local(''),
|
||||
url('../fonts/roboto-v29-latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-v29-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-v29-latin-500.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* roboto-700 - latin */
|
||||
@ -36,6 +33,5 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''),
|
||||
url('../fonts/roboto-v29-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-v29-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-v29-latin-700.woff2') format('woff2');
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -56,12 +56,12 @@
|
||||
"publish_dialog_title_topic": "أنشُر إلى {{topic}}",
|
||||
"publish_dialog_title_no_topic": "انشُر الإشعار",
|
||||
"publish_dialog_emoji_picker_show": "اختر رمزًا تعبيريًا",
|
||||
"publish_dialog_priority_min": "الحد الأدنى للأولوية",
|
||||
"publish_dialog_priority_min": "أولوية دنيا",
|
||||
"publish_dialog_priority_low": "أولوية منخفضة",
|
||||
"publish_dialog_priority_default": "الأولوية الافتراضية",
|
||||
"publish_dialog_priority_high": "أولوية عالية",
|
||||
"publish_dialog_base_url_label": "الرابط التشعبي للخدمة",
|
||||
"publish_dialog_priority_max": "الأولوية القصوى",
|
||||
"publish_dialog_priority_max": "أولوية قصوى",
|
||||
"publish_dialog_topic_placeholder": "اسم الموضوع، على سبيل المثال phil_alerts",
|
||||
"publish_dialog_title_label": "العنوان",
|
||||
"publish_dialog_title_placeholder": "عنوان الإشعار، على سبيل المثال تنبيه مساحة القرص",
|
||||
@ -154,7 +154,7 @@
|
||||
"subscribe_dialog_subscribe_button_cancel": "إلغاء",
|
||||
"subscribe_dialog_login_button_back": "العودة",
|
||||
"prefs_notifications_sound_play": "تشغيل الصوت المحدد",
|
||||
"prefs_notifications_min_priority_title": "الحد الأدنى للأولوية",
|
||||
"prefs_notifications_min_priority_title": "أولوية دنيا",
|
||||
"prefs_notifications_min_priority_max_only": "الأولوية القصوى فقط",
|
||||
"notifications_no_subscriptions_description": "انقر فوق الرابط \"{{linktext}}\" لإنشاء موضوع أو الاشتراك فيه. بعد ذلك، يمكنك إرسال رسائل عبر PUT أو POST وستتلقى إشعارات هنا.",
|
||||
"publish_dialog_click_label": "الرابط التشعبي URL للنقر",
|
||||
@ -296,5 +296,38 @@
|
||||
"prefs_users_description_no_sync": "لا تتم مزامنة المستخدمين وكلمات المرور مع حسابك.",
|
||||
"reservation_delete_dialog_action_delete_description": "سيتم حذف الرسائل والمرفقات المخزنة مؤقتا نهائيا. لا يمكن التراجع عن هذا الإجراء.",
|
||||
"notifications_actions_http_request_title": "إرسال طلب HTTP {{method}} إلى {{url}}",
|
||||
"notifications_none_for_any_description": "لإرسال إشعارات إلى موضوع ما، ما عليك سوى إرسال طلب PUT أو POST إلى الرابط التشعبي URL للموضوع. إليك مثال باستخدام أحد مواضيعك."
|
||||
"notifications_none_for_any_description": "لإرسال إشعارات إلى موضوع ما، ما عليك سوى إرسال طلب PUT أو POST إلى الرابط التشعبي URL للموضوع. إليك مثال باستخدام أحد مواضيعك.",
|
||||
"error_boundary_description": "من الواضح أن هذا لا ينبغي أن يحدث. آسف جدًا بشأن هذا. <br/> إن كان لديك دقيقة، يرجى <githubLink> الإبلاغ عن ذلك على GitHub </githubLink> ، أو إعلامنا عبر <discordLink> Discord </discordLink> أو <matrixLink> Matrix </matrixLink>.",
|
||||
"nav_button_muted": "الإشعارات المكتومة",
|
||||
"priority_min": "دنيا",
|
||||
"signup_error_username_taken": "تم حجز اسم المستخدم {{username}} مِن قَبلُ",
|
||||
"action_bar_reservation_limit_reached": "بلغت الحد الأقصى",
|
||||
"prefs_reservations_delete_button": "إعادة تعيين الوصول إلى الموضوع",
|
||||
"prefs_reservations_edit_button": "تعديل الوصول إلى موضوع",
|
||||
"prefs_reservations_limit_reached": "لقد بلغت الحد الأقصى من المواضيع المحجوزة.",
|
||||
"reservation_delete_dialog_action_keep_description": "ستصبح الرسائل والمرفقات المخزنة مؤقتًا على الخادم مرئية للعموم وللأشخاص الذين لديهم معرفة باسم الموضوع.",
|
||||
"reservation_delete_dialog_description": "تؤدي إزالة الحجز إلى التخلي عن ملكية الموضوع، مما يسمح للآخرين بحجزه. يمكنك الاحتفاظ بالرسائل والمرفقات الموجودة أو حذفها.",
|
||||
"prefs_reservations_dialog_description": "يمنحك حجز موضوع ما ملكية الموضوع، ويسمح لك بتحديد تصريحات وصول المستخدمين الآخرين إليه.",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "توفير ما يصل إلى {{discount}}٪",
|
||||
"account_upgrade_dialog_interval_monthly": "شهريا",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "إجمالي مساحة التخزين {{totalsize}}",
|
||||
"publish_dialog_progress_uploading_detail": "تحميل {{loaded}}/{{total}} ({{percent}}٪) …",
|
||||
"account_basics_tier_interval_monthly": "شهريا",
|
||||
"account_basics_tier_interval_yearly": "سنويا",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} مواضيع محجوزة",
|
||||
"account_upgrade_dialog_billing_contact_website": "للأسئلة المتعلقة بالفوترة، يرجى الرجوع إلى <Link>موقعنا على الويب</Link>.",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "إظهار الإشعارات إذا كانت الأولوية {{number}} ({{name}}) أو أعلى",
|
||||
"account_upgrade_dialog_billing_contact_email": "للأسئلة المتعلقة بالفوترة، الرجاء <Link>الاتصال بنا</Link> مباشرة.",
|
||||
"account_upgrade_dialog_tier_selected_label": "المحدد",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} لكل ملف",
|
||||
"account_upgrade_dialog_interval_yearly": "سنويا",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "لا توجد مواضيع محجوزة",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "وفر {{discount}}٪",
|
||||
"publish_dialog_click_reset": "إزالة الرابط التشعبي URL للنقر",
|
||||
"prefs_notifications_min_priority_description_max": "إظهار الإشعارات إذا كانت الأولوية 5 (كحد أقصى)",
|
||||
"publish_dialog_attachment_limits_file_reached": "يتجاوز الحد الأقصى للملف {{fileSizeLimit}}",
|
||||
"publish_dialog_attachment_limits_quota_reached": "يتجاوز الحصة، {{remainingBytes}} متبقية",
|
||||
"account_basics_tier_paid_until": "تم دفع مبلغ الاشتراك إلى غاية {{date}}، وسيتم تجديده تِلْقائيًا",
|
||||
"account_basics_tier_canceled_subscription": "تم إلغاء اشتراكك وسيتم إعادته إلى مستوى حساب مجاني بداية مِن {{date}}.",
|
||||
"account_delete_dialog_billing_warning": "إلغاء حسابك أيضاً يلغي اشتراكك في الفوترة فوراً ولن تتمكن من الوصول إلى لوح الفوترة بعد الآن."
|
||||
}
|
||||
|
@ -228,5 +228,25 @@
|
||||
"account_basics_username_description": "Хей, това сте вие ❤",
|
||||
"account_basics_username_admin_tooltip": "Вие сте администратор",
|
||||
"account_basics_password_title": "Парола",
|
||||
"account_delete_dialog_label": "Парола"
|
||||
"account_delete_dialog_label": "Парола",
|
||||
"account_basics_password_dialog_title": "Смяна на парола",
|
||||
"account_basics_password_dialog_current_password_label": "Текуща парола",
|
||||
"account_basics_password_dialog_new_password_label": "Нова парола",
|
||||
"account_basics_password_dialog_confirm_password_label": "Парола отново",
|
||||
"account_basics_password_dialog_button_submit": "Смяна на парола",
|
||||
"account_usage_title": "Употреба",
|
||||
"account_usage_of_limit": "от {{limit}}",
|
||||
"account_usage_unlimited": "Неограничено",
|
||||
"account_usage_limits_reset_daily": "Ограниченията се нулират всеки ден в полунощ (UTC)",
|
||||
"account_basics_tier_interval_monthly": "месечно",
|
||||
"account_basics_tier_interval_yearly": "годишно",
|
||||
"account_basics_password_description": "Промяна на паролата на профила",
|
||||
"account_basics_tier_title": "Вид на профила",
|
||||
"account_basics_tier_admin": "Администратор",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(с {{tier}} ниво)",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(без ниво)",
|
||||
"account_basics_tier_free": "безплатен",
|
||||
"account_basics_tier_basic": "базов",
|
||||
"account_basics_tier_change_button": "Променяне",
|
||||
"account_basics_tier_paid_until": "Абонаментът е платен до {{date}} и автоматично ще се поднови"
|
||||
}
|
||||
|
@ -285,7 +285,7 @@
|
||||
"account_delete_dialog_button_submit": "Trvale odstranit účet",
|
||||
"account_delete_dialog_billing_warning": "Odstraněním účtu se také okamžitě zruší vaše předplatné. Nebudete již mít přístup k fakturačnímu panelu.",
|
||||
"account_upgrade_dialog_title": "Změna úrovně účtu",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prohlášení</strong>: Při přechodu mezi placenými úrovněmi bude rozdíl v ceně účtován nebo vrácen v následující faktuře. Další fakturu obdržíte až na konci dalšího zúčtovacího období.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prohlášení</strong>: Při přechodu mezi placenými úrovněmi bude rozdíl v ceně <strong>zaúčtován okamžitě</strong>. Při přechodu na nižší úroveň se zůstatek použije na platbu za budoucí zúčtovací období.",
|
||||
"account_upgrade_dialog_reservations_warning_one": "Vybraná úroveň umožňuje méně rezervovaných témat než vaše aktuální úroveň. Než změníte svou úroveň, <strong>odstraňte alespoň jednu rezervaci</strong>. Rezervace můžete odstranit v <Link>Nastavení</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} rezervovaných témat",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} denních zpráv",
|
||||
@ -340,5 +340,17 @@
|
||||
"reservation_delete_dialog_action_keep_description": "Zprávy a přílohy, které jsou uloženy v mezipaměti serveru, se stanou veřejně viditelnými pro osoby, které znají název tématu.",
|
||||
"reservation_delete_dialog_action_delete_title": "Odstranění zpráv a příloh uložených v mezipaměti",
|
||||
"reservation_delete_dialog_action_delete_description": "Zprávy a přílohy uložené v mezipaměti budou trvale odstraněny. Tuto akci nelze vrátit zpět.",
|
||||
"reservation_delete_dialog_submit_button": "Odstranit rezervaci"
|
||||
"reservation_delete_dialog_submit_button": "Odstranit rezervaci",
|
||||
"account_basics_tier_interval_yearly": "roční",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "ušetříte {{discount}}%",
|
||||
"account_upgrade_dialog_tier_price_per_month": "měsíc",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Žádná rezervovaná témata",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "ušetříte až {{discount}}%",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} účtováno ročně. Ušetříte {{save}}.",
|
||||
"account_basics_tier_interval_monthly": "měsíční",
|
||||
"account_upgrade_dialog_interval_monthly": "Měsíční",
|
||||
"account_upgrade_dialog_interval_yearly": "Roční",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} za rok. Účtuje se měsíčně.",
|
||||
"account_upgrade_dialog_billing_contact_email": "V případě dotazů týkajících se fakturace nás prosím <Link>kontaktujte</Link> přímo.",
|
||||
"account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich <Link>webových stránkách</Link>."
|
||||
}
|
||||
|
@ -221,5 +221,63 @@
|
||||
"account_tokens_delete_dialog_submit_button": "Slet token permanent",
|
||||
"prefs_notifications_delete_after_one_month": "Efter en måned",
|
||||
"prefs_notifications_delete_after_one_week": "Efter en uge",
|
||||
"prefs_users_dialog_username_label": "Brugernavn, f.eks. phil"
|
||||
"prefs_users_dialog_username_label": "Brugernavn, f.eks. phil",
|
||||
"prefs_notifications_delete_after_one_day_description": "Notifikationer slettes automatisk efter en dag",
|
||||
"notifications_none_for_topic_description": "For at sende en notifikation til dette emne, skal du blot sende en PUT eller POST til emne-URL'en.",
|
||||
"notifications_none_for_any_description": "For at sende en notifikation til et emne, skal du blot sende en PUT eller POST til emne-URL'en. Her er et eksempel med et af dine emner.",
|
||||
"notifications_no_subscriptions_title": "Det ser ud til, at du ikke har nogen abonnementer endnu.",
|
||||
"notifications_more_details": "For mere information, se <websiteLink>webstedet</websiteLink> eller <docsLink>dokumentationen</docsLink>.",
|
||||
"display_name_dialog_description": "Angiv et alternativt navn for et emne, der vises på abonnementslisten. Dette gør det nemmere at identificere emner med komplicerede navne.",
|
||||
"reserve_dialog_checkbox_label": "Reserver emne og konfigurer adgang",
|
||||
"publish_dialog_attachment_limits_file_reached": "overskrider {{fileSizeLimit}} filgrænse",
|
||||
"publish_dialog_attachment_limits_quota_reached": "overskrider kvote, {{remainingBytes}} tilbage",
|
||||
"publish_dialog_topic_label": "Emnenavn",
|
||||
"publish_dialog_topic_placeholder": "Emnenavn, f.eks. phil_alerts",
|
||||
"publish_dialog_topic_reset": "Nulstil emne",
|
||||
"publish_dialog_click_reset": "Fjern klik-URL",
|
||||
"publish_dialog_delay_placeholder": "Forsink levering, f.eks. {{unixTimestamp}}, {{relativeTime}} eller \"{{naturalLanguage}}\" (kun på engelsk)",
|
||||
"publish_dialog_other_features": "Andre funktioner:",
|
||||
"publish_dialog_chip_attach_url_label": "Vedhæft fil via URL",
|
||||
"publish_dialog_chip_attach_file_label": "Vedhæft lokal fil",
|
||||
"publish_dialog_details_examples_description": "For eksempler og en detaljeret beskrivelse af alle afsendelsesfunktioner henvises til <docsLink>dokumentationen</docsLink>.",
|
||||
"publish_dialog_button_cancel_sending": "Annuller afsendelse",
|
||||
"publish_dialog_attached_file_title": "Vedhæftet fil:",
|
||||
"emoji_picker_search_placeholder": "Søg emoji",
|
||||
"emoji_picker_search_clear": "Ryd søgning",
|
||||
"subscribe_dialog_subscribe_title": "Abonner på emne",
|
||||
"subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_alerts",
|
||||
"subscribe_dialog_subscribe_button_generate_topic_name": "Generer navn",
|
||||
"subscribe_dialog_login_title": "Login påkrævet",
|
||||
"subscribe_dialog_login_description": "Dette emne er adgangskodebeskyttet. Indtast venligst brugernavn og adgangskode for at abonnere.",
|
||||
"subscribe_dialog_error_user_not_authorized": "Brugeren {{username}} er ikke autoriseret",
|
||||
"account_basics_password_description": "Skift adgangskoden til din konto",
|
||||
"account_usage_limits_reset_daily": "Brugsgrænser nulstilles dagligt ved midnat (UTC)",
|
||||
"account_basics_tier_paid_until": "Abonnementet er betalt indtil {{date}} og fornys automatisk",
|
||||
"account_basics_tier_payment_overdue": "Din betaling er forfalden. Opdater venligst din betalingsmetode, ellers bliver din konto snart nedgraderet.",
|
||||
"account_basics_tier_canceled_subscription": "Dit abonnement blev annulleret og vil blive nedgraderet til en gratis konto den {{date}}.",
|
||||
"account_usage_cannot_create_portal_session": "Kan ikke åbne faktureringsportalen",
|
||||
"account_delete_description": "Slet din konto permanent",
|
||||
"account_delete_dialog_description": "Dette vil slette din konto permanent, inklusive alle data, der er gemt på serveren. Efter sletning vil dit brugernavn være utilgængeligt i 7 dage. Hvis du virkelig ønsker at fortsætte, bedes du bekræfte med dit kodeord i feltet nedenfor.",
|
||||
"account_upgrade_dialog_button_pay_now": "Betal nu og abonner",
|
||||
"account_tokens_table_last_origin_tooltip": "Fra IP-adresse {{ip}}, klik for at slå op",
|
||||
"account_tokens_dialog_label": "Label, f.eks. radarmeddelelser",
|
||||
"account_tokens_dialog_expires_label": "Adgangstoken udløber om",
|
||||
"account_tokens_dialog_expires_unchanged": "Lad udløbsdatoen forblive uændret",
|
||||
"account_tokens_dialog_expires_x_hours": "Token udløber om {{hours}} timer",
|
||||
"account_tokens_dialog_expires_x_days": "Token udløber om {{days}} dage",
|
||||
"prefs_notifications_sound_description_none": "Notifikationer afspiller ingen lyd, når de ankommer",
|
||||
"prefs_notifications_sound_description_some": "Notifikationer afspiller {{sound}}-lyden, når de ankommer",
|
||||
"prefs_notifications_min_priority_low_and_higher": "Lav prioritet og højere",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Standardprioritet og højere",
|
||||
"prefs_notifications_min_priority_high_and_higher": "Høj prioritet og højere",
|
||||
"prefs_notifications_delete_after_never_description": "Notifikationer slettes aldrig automatisk",
|
||||
"prefs_notifications_delete_after_three_hours_description": "Notifikationer slettes automatisk efter tre timer",
|
||||
"prefs_notifications_delete_after_one_week_description": "Notifikationer slettes automatisk efter en uge",
|
||||
"prefs_notifications_delete_after_one_month_description": "Notifikationer slettes automatisk efter en måned",
|
||||
"prefs_reservations_limit_reached": "Du har nået din grænse for reserverede emner.",
|
||||
"prefs_reservations_table_click_to_subscribe": "Klik for at abonnere",
|
||||
"reservation_delete_dialog_action_keep_title": "Behold cachelagrede meddelelser og vedhæftede filer",
|
||||
"reservation_delete_dialog_action_delete_title": "Slet cachelagrede meddelelser og vedhæftede filer",
|
||||
"error_boundary_title": "Oh nej, ntfy brød sammen",
|
||||
"error_boundary_description": "Dette bør naturligvis ikke ske. Det beklager vi meget.<br/>Hvis du har et øjeblik, bedes du <githubLink>rapportere dette på GitHub</githubLink>, eller give os besked via <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>."
|
||||
}
|
||||
|
@ -82,7 +82,7 @@
|
||||
"publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
|
||||
"publish_dialog_filename_placeholder": "Dateiname des Anhangs",
|
||||
"publish_dialog_delay_label": "Verzögerung",
|
||||
"publish_dialog_email_placeholder": "E-Mail-Adresse, an die die Benachrichtigung gesendet werden soll, z. B. phil@example.com",
|
||||
"publish_dialog_email_placeholder": "E-Mail-Adresse, an welche die Benachrichtigung gesendet werden soll, z. B. phil@example.com",
|
||||
"publish_dialog_chip_click_label": "Klick-URL",
|
||||
"publish_dialog_button_cancel_sending": "Senden abbrechen",
|
||||
"publish_dialog_drop_file_here": "Datei hierher ziehen",
|
||||
@ -261,7 +261,7 @@
|
||||
"account_usage_basis_ip_description": "Nutzungsstatistiken und Limits für diesen Account basieren auf Deiner IP-Adresse, können also mit anderen Usern geteilt sein. Die oben gezeigten Limits sind Schätzungen basierend auf den bestehenden Limits.",
|
||||
"account_delete_dialog_billing_warning": "Das Löschen Deines Kontos storniert auch sofort Deine Zahlung. Du wirst dann keinen Zugang zum Abrechnungs-Dashboard haben.",
|
||||
"account_upgrade_dialog_title": "Konto-Level ändern",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Anrechnung</strong>: Wenn Du zwischen kostenpflichtigen Leveln wechselst wir die Differenz bei der nächsten Abrechnung nachberechnet oder erstattet. Du erhältst bis zum Ende der Abrechnungsperiode keine neue Rechnung.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Anrechnung</strong>: Wenn Du auf einen höheren kostenpflichtigen Level wechselst wird die Differenz <strong>sofort berechnet</strong>. Beim Wechsel auf ein kleineres Level verwenden wir Dein Guthaben für zukünftige Abrechnungsperioden.",
|
||||
"account_upgrade_dialog_reservations_warning_one": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens eine Reservierung</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Das gewählte Level erlaubt weniger reservierte Themen als Dein aktueller Level. <strong>Bitte löschen vor dem Wechsel Deines Levels mindestens {{count}} Reservierungen</strong>. Du kannst Reservierungen in den <Link>Einstellungen</Link> löschen.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} reservierte Themen",
|
||||
@ -340,5 +340,17 @@
|
||||
"nav_upgrade_banner_label": "Upgrade auf ntfy Pro",
|
||||
"alert_not_supported_context_description": "Benachrichtigungen werden nur über HTTPS unterstützt. Das ist eine Einschränkung der <mdnLink>Notifications API</mdnLink>.",
|
||||
"display_name_dialog_description": "Lege einen alternativen Namen für ein Thema fest, der in der Abo-Liste angezeigt wird. So kannst Du Themen mit komplizierten Namen leichter finden.",
|
||||
"account_basics_username_admin_tooltip": "Du bist Admin"
|
||||
"account_basics_username_admin_tooltip": "Du bist Admin",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "spare {{discount}}%",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "spare bis zu {{discount}}%",
|
||||
"account_upgrade_dialog_tier_price_per_month": "Monat",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} pro Jahr. Spare {{save}}.",
|
||||
"account_upgrade_dialog_billing_contact_email": "Bei Fragen zur Abrechnung, <Link>kontaktiere uns</Link> bitte direkt.",
|
||||
"account_upgrade_dialog_billing_contact_website": "Bei Fragen zur Abrechnung sieh bitte auf unserer <Link>Webseite</Link> nach.",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Keine reservierten Themen",
|
||||
"account_basics_tier_interval_yearly": "jährlich",
|
||||
"account_basics_tier_interval_monthly": "monatlich",
|
||||
"account_upgrade_dialog_interval_monthly": "Monatlich",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} pro Jahr. Monatlich abgerechnet.",
|
||||
"account_upgrade_dialog_interval_yearly": "Jährlich"
|
||||
}
|
||||
|
@ -293,7 +293,7 @@
|
||||
"account_delete_dialog_button_submit": "Eliminar permanentemente la cuenta",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} tópicos reservados",
|
||||
"account_upgrade_dialog_cancel_warning": "Esto <strong>cancelará su suscripción</strong> y degradará su cuenta en {{date}}. En esa fecha, sus tópicos reservados y sus mensajes almacenados en caché en el servidor <strong>serán eliminados</strong>.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorrateo</strong>: Al cambiar entre planes de pago, la diferencia de precio se cargará o reembolsará en la siguiente factura. No recibirá otra factura hasta el final del siguiente periodo de facturación.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorrateo</strong>: al actualizar entre planes pagos, la diferencia de precio se <strong>cobrará de inmediato</strong>. Al cambiar a un nivel inferior, el saldo se utilizará para pagar futuros períodos de facturación.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "El nivel seleccionado permite menos tópicos reservados que su nivel actual. Antes de cambiar de nivel, <strong>por favor elimine al menos {{count}} reservaciones</strong>. Puede eliminar reservaciones en <Link>Configuración</Link>.",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} mensajes diarios",
|
||||
"account_upgrade_dialog_tier_features_emails": "{{emails}} correos diarios",
|
||||
@ -340,5 +340,17 @@
|
||||
"prefs_reservations_dialog_topic_label": "Tópico",
|
||||
"reservation_delete_dialog_description": "Al eliminar una reserva se renuncia a la propiedad sobre el tópico y se permite que otros lo reserven. Puede conservar o eliminar los mensajes y archivos adjuntos existentes.",
|
||||
"reservation_delete_dialog_action_delete_title": "Eliminar mensajes y archivos adjuntos en caché",
|
||||
"reservation_delete_dialog_submit_button": "Eliminar reserva"
|
||||
"reservation_delete_dialog_submit_button": "Eliminar reserva",
|
||||
"account_basics_tier_interval_monthly": "mensualmente",
|
||||
"account_basics_tier_interval_yearly": "anualmente",
|
||||
"account_upgrade_dialog_interval_monthly": "Mensualmente",
|
||||
"account_upgrade_dialog_interval_yearly": "Anualmente",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "ahorrar {{discount}}%",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "ahorra hasta un {{discount}}%",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Ningún tema reservado",
|
||||
"account_upgrade_dialog_tier_price_per_month": "mes",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} facturado anualmente. Guardar {{save}}.",
|
||||
"account_upgrade_dialog_billing_contact_website": "Si tiene preguntas sobre facturación, consulte nuestra <Link>página web</Link>.",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} al año. Facturación mensual.",
|
||||
"account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor <Link>contáctenos</Link> directamente."
|
||||
}
|
||||
|
@ -256,7 +256,7 @@
|
||||
"account_usage_cannot_create_portal_session": "Tidak dapat membuka portal tagihan",
|
||||
"account_delete_dialog_billing_warning": "Menghapus akun Anda juga membatalkan tagihan langganan dengan segera. Anda tidak akan memiliki akses lagi ke dasbor tagihan.",
|
||||
"account_upgrade_dialog_title": "Ubah peringkat akun",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorasi</strong>: Ketika mengubah rencana berbayar, perubahan harga akan ditagih atau dikembalikan di faktur berikutnya. Anda tidak akan menerima faktur lain sampai akhir periode tagihan.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Prorasi</strong>: Saat melakukan upgrade antar paket berbayar, selisih harga akan <strong>langsung dibebankan ke</strong>. Saat menurunkan ke tingkat yang lebih rendah, saldo akan digunakan untuk membayar periode penagihan di masa mendatang.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Peringkat yang dipilih memperbolehkan lebih sedikit reservasi topik daripada peringkat Anda saat ini. Sebelum mengubah peringkat Anda, <strong>silakan menghapus setidaknya {{count}} reservasi</strong>. Anda dapat menghapus reservasi di <Link>Pengaturan</Link>.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} topik yang telah direservasi",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} pesan harian",
|
||||
@ -340,5 +340,17 @@
|
||||
"prefs_reservations_dialog_description": "Mereservasikan sebuah topik memberikan Anda kemilikan pada topik, dan memungkinkan Anda untuk mendefinisikan perizinan akses untuk pengguna lain melalui topik.",
|
||||
"prefs_reservations_dialog_topic_label": "Topik",
|
||||
"prefs_reservations_dialog_access_label": "Akses",
|
||||
"reservation_delete_dialog_description": "Menghapus sebuah reservasi menghapus kemilikan pada topik, dan memperbolehkan orang-orang lain untuk mereservasinya."
|
||||
"reservation_delete_dialog_description": "Menghapus sebuah reservasi menghapus kemilikan pada topik, dan memperbolehkan orang-orang lain untuk mereservasinya.",
|
||||
"account_upgrade_dialog_interval_yearly": "Setiap tahun",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "Ditagih {{price}} setiap tahun. Hemat {{save}}.",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "hemat {{discount}}%",
|
||||
"account_upgrade_dialog_interval_monthly": "Setiap bulan",
|
||||
"account_basics_tier_interval_monthly": "setiap bulan",
|
||||
"account_basics_tier_interval_yearly": "setiap tahun",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "hemat sampai {{discount}}%",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Tidak ada topik yang direservasi",
|
||||
"account_upgrade_dialog_tier_price_per_month": "bulan",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} per bulan. Ditagih setiap bulan.",
|
||||
"account_upgrade_dialog_billing_contact_email": "Untuk pertanyaan penagihan, silakan <Link>hubungi kami</Link> secara langsung.",
|
||||
"account_upgrade_dialog_billing_contact_website": "Untuk pertanyaan penagihan, silakan menuju ke <Link>situs web</Link> kami."
|
||||
}
|
||||
|
@ -235,5 +235,78 @@
|
||||
"account_usage_title": "Użycie",
|
||||
"account_usage_of_limit": "z {{limit}}",
|
||||
"account_usage_unlimited": "Bez limitu",
|
||||
"account_usage_limits_reset_daily": "Limity są resetowane codziennie o północy (UTC)"
|
||||
"account_usage_limits_reset_daily": "Limity są resetowane codziennie o północy (UTC)",
|
||||
"account_delete_dialog_button_submit": "Nieodwracalnie usuń konto",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Brak rezerwacji tematów",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} na plik",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} pamięci łącznie",
|
||||
"account_upgrade_dialog_tier_price_per_month": "miesiąc",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} na rok. Płatne miesięcznie.",
|
||||
"account_upgrade_dialog_billing_contact_email": "W razie pytań dotyczących rozliczeń <Link>skontaktuj się z nami</Link> bezpośrednio.",
|
||||
"account_upgrade_dialog_billing_contact_website": "W razie pytań dotyczących rozliczeń sprawdź naszą <Link>stronę</Link>.",
|
||||
"account_upgrade_dialog_button_cancel_subscription": "Anuluj subskrypcję",
|
||||
"account_upgrade_dialog_button_update_subscription": "Zmień subskrypcję",
|
||||
"account_tokens_title": "Tokeny dostępowe",
|
||||
"account_tokens_table_token_header": "Token",
|
||||
"account_tokens_table_label_header": "Etykieta",
|
||||
"account_tokens_table_last_access_header": "Ostatnie użycie",
|
||||
"account_tokens_table_expires_header": "Termin ważności",
|
||||
"account_tokens_table_never_expires": "Bezterminowy",
|
||||
"account_tokens_table_current_session": "Aktualna sesja przeglądarki",
|
||||
"account_tokens_table_copy_to_clipboard": "Kopiuj do schowka",
|
||||
"account_tokens_table_copied_to_clipboard": "Token został skopiowany",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Nie można edytować ani usunąć tokenu aktualnej sesji",
|
||||
"account_tokens_table_create_token_button": "Utwórz token dostępowy",
|
||||
"account_tokens_dialog_label": "Etykieta, np. Powiadomienia Radarr",
|
||||
"account_tokens_dialog_button_update": "Zmień token",
|
||||
"account_basics_tier_interval_monthly": "miesięcznie",
|
||||
"account_basics_tier_interval_yearly": "rocznie",
|
||||
"account_upgrade_dialog_interval_monthly": "Miesięcznie",
|
||||
"account_upgrade_dialog_title": "Zmień plan konta",
|
||||
"account_delete_dialog_description": "Konto, wraz ze wszystkimi związanymi z nim danymi przechowywanymi na serwerze, będzie nieodwracalnie usunięte. Po usunięciu Twoja nazwa użytkownika będzie niedostępna jeszcze przez 7 dni. Jeśli chcesz kontynuować, potwierdź wpisując swoje hasło w polu poniżej.",
|
||||
"account_delete_dialog_billing_warning": "Usunięcie konta powoduje natychmiastowe anulowanie subskrypcji. Nie będziesz już mieć dostępu do strony z rachunkami.",
|
||||
"account_upgrade_dialog_interval_yearly": "Rocznie",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "taniej o {{discount}}%",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "nawet {{discount}}% taniej",
|
||||
"account_upgrade_dialog_button_cancel": "Anuluj",
|
||||
"account_tokens_description": "Używaj tokenów do publikowania wiadomości i subskrybowania tematów przez API ntfy, żeby uniknąć konieczności podawania danych do logowania. Szczegóły znajdziesz w <Link>dokumentacji</Link>.",
|
||||
"account_tokens_dialog_title_create": "Utwórz token dostępowy",
|
||||
"account_tokens_table_last_origin_tooltip": "Z adresu IP {{ip}}, kliknij żeby sprawdzić",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} płatne jednorazowo. Oszczędzasz {{save}}.",
|
||||
"account_tokens_dialog_title_edit": "Edytuj token dostępowy",
|
||||
"account_tokens_dialog_title_delete": "Usuń token dostępowy",
|
||||
"account_tokens_dialog_button_create": "Utwórz token",
|
||||
"nav_upgrade_banner_label": "Przejdź na ntfy Pro",
|
||||
"nav_upgrade_banner_description": "Rezerwuj tematy, więcej powiadomień i maili oraz większe załączniki",
|
||||
"alert_not_supported_context_description": "Powiadomienia działają tylko przez HTTPS. To jest ograniczenie <mdnLink>Notifications API</mdnLink>.",
|
||||
"account_basics_tier_canceled_subscription": "Twoja subskrypcja została anulowana i konto zostanie ograniczone do wersji darmowej w dniu {{date}}.",
|
||||
"account_basics_tier_manage_billing_button": "Zarządzaj rachunkami",
|
||||
"account_usage_messages_title": "Wysłane wiadomości",
|
||||
"account_usage_emails_title": "Wysłane maile",
|
||||
"account_basics_tier_title": "Rodzaj konta",
|
||||
"account_basics_tier_description": "Mocarność Twojego konta",
|
||||
"account_basics_tier_admin": "Administrator",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(plan {{tier}})",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(brak planu)",
|
||||
"account_basics_tier_basic": "Podstawowe",
|
||||
"account_basics_tier_free": "Darmowe",
|
||||
"account_basics_tier_upgrade_button": "Przejdź na Pro",
|
||||
"account_basics_tier_change_button": "Zmień",
|
||||
"account_basics_tier_paid_until": "Subskrypcja opłacona do {{date}} i będzie odnowiona automatycznie",
|
||||
"account_basics_tier_payment_overdue": "Minął termin płatności. Zaktualizuj metodę płatności, w przeciwnym razie Twoje konto wkrótce zostanie ograniczone.",
|
||||
"account_usage_reservations_title": "Zarezerwowane tematy",
|
||||
"account_usage_reservations_none": "Brak zarezerwowanych tematów na tym koncie",
|
||||
"account_usage_attachment_storage_title": "Miejsce na załączniki",
|
||||
"account_usage_attachment_storage_description": "{{filesize}} na każdy plik, przechowywane przez {{expiry}}",
|
||||
"account_usage_basis_ip_description": "Statystyki i limity dla tego konta bazują na Twoim adresie IP, więc mogą być współdzielone z innymi użytkownikami. Limity pokazane powyżej to wartości przybliżone bazujące na rzeczywistych limitach.",
|
||||
"account_usage_cannot_create_portal_session": "Nie można otworzyć portalu z rachunkami",
|
||||
"account_delete_title": "Usuń konto",
|
||||
"account_delete_description": "Usuń swoje konto nieodwracalnie",
|
||||
"account_delete_dialog_label": "Hasło",
|
||||
"account_delete_dialog_button_cancel": "Anuluj",
|
||||
"account_upgrade_dialog_button_redirect_signup": "Załóż konto",
|
||||
"account_upgrade_dialog_button_pay_now": "Zapłać i aktywuj subskrypcję",
|
||||
"account_tokens_dialog_button_cancel": "Anuluj",
|
||||
"account_tokens_dialog_expires_label": "Token dostępowy wygasa po",
|
||||
"account_tokens_dialog_expires_unchanged": "Pozostaw termin ważności bez zmian"
|
||||
}
|
||||
|
@ -251,7 +251,7 @@
|
||||
"account_delete_dialog_button_submit": "Hesabı kalıcı olarak sil",
|
||||
"account_delete_dialog_billing_warning": "Hesabınızı silmek, faturalandırma aboneliğinizi de anında iptal eder. Artık faturalandırma sayfasına erişiminiz olmayacak.",
|
||||
"account_upgrade_dialog_title": "Hesap seviyesini değiştir",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Ödeme oranı</strong>: Ücretli planlar arasında geçiş yaparken, fiyat farkı bir sonraki faturada tahsil edilecek veya iade edilecektir. Bir sonraki fatura döneminin sonuna kadar başka bir fatura almayacaksınız.",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Fiyatlandırma</strong>: Ücretli planlar arasında yükseltme yaparken, fiyat farkı <strong>hemen tahsil edilecektir</strong>. Daha düşük bir seviyeye inildiğinde, bakiye gelecek faturalandırma dönemleri için ödeme yapmak üzere kullanılacaktır.",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Seçilen seviye, geçerli seviyenizden daha az konu ayırtmaya izin veriyor. Seviyenizi değiştirmeden önce <strong>lütfen en az {{count}} ayırtmayı silin</strong>. Ayırtmaları <Link>Ayarlar</Link> sayfasından kaldırabilirsiniz.",
|
||||
"account_upgrade_dialog_tier_features_reservations": "{{reservations}} konu ayırtıldı",
|
||||
"account_upgrade_dialog_tier_features_messages": "{{messages}} günlük mesaj",
|
||||
@ -340,5 +340,17 @@
|
||||
"prefs_reservations_table_everyone_read_only": "Ben yayınlayabilir ve abone olabilirim, herkes abone olabilir",
|
||||
"prefs_reservations_table_not_subscribed": "Abone olunmadı",
|
||||
"prefs_reservations_table_everyone_read_write": "Herkes yayınlayabilir ve abone olabilir",
|
||||
"reservation_delete_dialog_description": "Ayırtmanın kaldırılması, konu üzerindeki sahiplikten vazgeçer ve başkalarının onu ayırtmasına izin verir. Mevcut mesajları ve ekleri saklayabilir veya silebilirsiniz."
|
||||
"reservation_delete_dialog_description": "Ayırtmanın kaldırılması, konu üzerindeki sahiplikten vazgeçer ve başkalarının onu ayırtmasına izin verir. Mevcut mesajları ve ekleri saklayabilir veya silebilirsiniz.",
|
||||
"account_basics_tier_interval_yearly": "yıllık",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Ayırtılan konu yok",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "Yıllık {{price}}. Aylık faturalandırılır.",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} yıllık olarak faturalandırılır. {{save}} tasarruf edin.",
|
||||
"account_upgrade_dialog_interval_yearly": "Yıllık",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "%{{discount}} tasarruf edin",
|
||||
"account_upgrade_dialog_tier_price_per_month": "ay",
|
||||
"account_upgrade_dialog_billing_contact_email": "Faturalama ile ilgili sorularınız için lütfen doğrudan <Link>bizimle iletişime geçin</Link>.",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "%{{discount}} kadar tasarruf edin",
|
||||
"account_upgrade_dialog_interval_monthly": "Aylık",
|
||||
"account_basics_tier_interval_monthly": "aylık",
|
||||
"account_upgrade_dialog_billing_contact_website": "Faturalama ile ilgili sorularınız için lütfen <Link>web sitemizi ziyaret edin</Link>."
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs} from "./utils";
|
||||
|
||||
const retryBackoffSeconds = [5, 10, 15, 20, 30];
|
||||
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
||||
|
||||
/**
|
||||
* A connection contains a single WebSocket connection for one topic. It handles its connection
|
||||
|
Loading…
Reference in New Issue
Block a user