mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-05-17 19:03:29 +12:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
4e1980c2cc
22
.github/workflows/build.yaml
vendored
22
.github/workflows/build.yaml
vendored
|
@ -1,30 +1,24 @@
|
|||
name: build
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Install dependencies
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build all the things
|
||||
- name: Build all the things
|
||||
run: make build
|
||||
-
|
||||
name: Print build results and checksums
|
||||
- name: Print build results and checksums
|
||||
run: make cli-build-results
|
||||
|
|
23
.github/workflows/release.yaml
vendored
23
.github/workflows/release.yaml
vendored
|
@ -7,35 +7,28 @@ jobs:
|
|||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Docker login
|
||||
- name: Docker login
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
-
|
||||
name: Install dependencies
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build and publish
|
||||
- name: Build and publish
|
||||
run: make release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Print build results and checksums
|
||||
- name: Print build results and checksums
|
||||
run: make cli-build-results
|
||||
|
|
31
.github/workflows/test.yaml
vendored
31
.github/workflows/test.yaml
vendored
|
@ -1,39 +1,30 @@
|
|||
name: test
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Install dependencies
|
||||
- name: Install dependencies
|
||||
run: make build-deps-ubuntu
|
||||
-
|
||||
name: Build docs (required for tests)
|
||||
- name: Build docs (required for tests)
|
||||
run: make docs
|
||||
-
|
||||
name: Build web app (required for tests)
|
||||
- name: Build web app (required for tests)
|
||||
run: make web
|
||||
-
|
||||
name: Run tests, formatting, vetting and linting
|
||||
- name: Run tests, formatting, vetting and linting
|
||||
run: make check
|
||||
-
|
||||
name: Run coverage
|
||||
- name: Run coverage
|
||||
run: make coverage
|
||||
-
|
||||
name: Upload coverage to codecov.io
|
||||
- name: Upload coverage to codecov.io
|
||||
run: make coverage-upload
|
||||
|
|
|
@ -9,7 +9,8 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|||
LABEL org.opencontainers.image.title="ntfy"
|
||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN apk add --no-cache tzdata \
|
||||
&& adduser -D -u 1000 ntfy
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
|
|
@ -12,6 +12,7 @@ LABEL org.opencontainers.image.description="Send push notifications to your phon
|
|||
# Alpine does not support adding "tzdata" on ARM anymore, see
|
||||
# https://github.com/binwiederhier/ntfy/issues/894
|
||||
|
||||
RUN adduser -D -u 1000 ntfy
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
|
|
@ -53,6 +53,7 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|||
LABEL org.opencontainers.image.title="ntfy"
|
||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
|
||||
RUN adduser -D -u 1000 ntfy
|
||||
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
|
164
cmd/serve.go
164
cmd/serve.go
|
@ -6,23 +6,22 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/v2/log"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -35,7 +34,7 @@ const (
|
|||
|
||||
var flagsServe = append(
|
||||
append([]cli.Flag{}, flagsDefault...),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, Usage: "config file"},
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
|
||||
|
@ -45,19 +44,19 @@ var flagsServe = append(
|
|||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: util.FormatDuration(server.DefaultCacheDuration), Usage: "buffer messages for this time to allow `since` requests"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Value: util.FormatDuration(server.DefaultCacheBatchTimeout), Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
|
||||
|
@ -76,16 +75,18 @@ 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-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: "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.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"visitor_attachment_daily_bandwidth_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorRequestLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
|
||||
|
@ -126,7 +127,7 @@ func execServe(c *cli.Context) error {
|
|||
|
||||
// Read all the options
|
||||
config := c.String("config")
|
||||
baseURL := c.String("base-url")
|
||||
baseURL := strings.TrimSuffix(c.String("base-url"), "/")
|
||||
listenHTTP := c.String("listen-http")
|
||||
listenHTTPS := c.String("listen-https")
|
||||
listenUnix := c.String("listen-unix")
|
||||
|
@ -140,19 +141,19 @@ func execServe(c *cli.Context) error {
|
|||
webPushEmailAddress := c.String("web-push-email-address")
|
||||
webPushStartupQueries := c.String("web-push-startup-queries")
|
||||
cacheFile := c.String("cache-file")
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
cacheDurationStr := c.String("cache-duration")
|
||||
cacheStartupQueries := c.String("cache-startup-queries")
|
||||
cacheBatchSize := c.Int("cache-batch-size")
|
||||
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||
cacheBatchTimeoutStr := c.String("cache-batch-timeout")
|
||||
authFile := c.String("auth-file")
|
||||
authStartupQueries := c.String("auth-startup-queries")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
|
||||
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
|
||||
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
|
||||
keepaliveInterval := c.Duration("keepalive-interval")
|
||||
managerInterval := c.Duration("manager-interval")
|
||||
attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
|
||||
keepaliveIntervalStr := c.String("keepalive-interval")
|
||||
managerIntervalStr := c.String("manager-interval")
|
||||
disallowedTopics := c.StringSlice("disallowed-topics")
|
||||
webRoot := c.String("web-root")
|
||||
enableSignup := c.Bool("enable-signup")
|
||||
|
@ -171,17 +172,19 @@ func execServe(c *cli.Context) error {
|
|||
twilioAuthToken := c.String("twilio-auth-token")
|
||||
twilioPhoneNumber := c.String("twilio-phone-number")
|
||||
twilioVerifyService := c.String("twilio-verify-service")
|
||||
messageSizeLimitStr := c.String("message-size-limit")
|
||||
messageDelayLimitStr := c.String("message-delay-limit")
|
||||
totalTopicLimit := c.Int("global-topic-limit")
|
||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
||||
visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
|
||||
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
||||
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
|
||||
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
||||
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
|
||||
visitorRequestLimitReplenishStr := c.String("visitor-request-limit-replenish")
|
||||
visitorRequestLimitExemptHosts := util.SplitNoEmpty(c.String("visitor-request-limit-exempt-hosts"), ",")
|
||||
visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
|
||||
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
|
||||
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
|
||||
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
|
||||
behindProxy := c.Bool("behind-proxy")
|
||||
stripeSecretKey := c.String("stripe-secret-key")
|
||||
stripeWebhookKey := c.String("stripe-webhook-key")
|
||||
|
@ -190,6 +193,64 @@ func execServe(c *cli.Context) error {
|
|||
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
|
||||
profileListenHTTP := c.String("profile-listen-http")
|
||||
|
||||
// Convert durations
|
||||
cacheDuration, err := util.ParseDuration(cacheDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cache duration: %s", cacheDurationStr)
|
||||
}
|
||||
cacheBatchTimeout, err := util.ParseDuration(cacheBatchTimeoutStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid cache batch timeout: %s", cacheBatchTimeoutStr)
|
||||
}
|
||||
attachmentExpiryDuration, err := util.ParseDuration(attachmentExpiryDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment expiry duration: %s", attachmentExpiryDurationStr)
|
||||
}
|
||||
keepaliveInterval, err := util.ParseDuration(keepaliveIntervalStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid keepalive interval: %s", keepaliveIntervalStr)
|
||||
}
|
||||
managerInterval, err := util.ParseDuration(managerIntervalStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid manager interval: %s", managerIntervalStr)
|
||||
}
|
||||
messageDelayLimit, err := util.ParseDuration(messageDelayLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid message delay limit: %s", messageDelayLimitStr)
|
||||
}
|
||||
visitorRequestLimitReplenish, err := util.ParseDuration(visitorRequestLimitReplenishStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor request limit replenish: %s", visitorRequestLimitReplenishStr)
|
||||
}
|
||||
visitorEmailLimitReplenish, err := util.ParseDuration(visitorEmailLimitReplenishStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
|
||||
}
|
||||
|
||||
// Convert sizes to bytes
|
||||
messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid message size limit: %s", messageSizeLimitStr)
|
||||
}
|
||||
attachmentTotalSizeLimit, err := util.ParseSize(attachmentTotalSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment total size limit: %s", attachmentTotalSizeLimitStr)
|
||||
}
|
||||
attachmentFileSizeLimit, err := util.ParseSize(attachmentFileSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment file size limit: %s", attachmentFileSizeLimitStr)
|
||||
}
|
||||
visitorAttachmentTotalSizeLimit, err := util.ParseSize(visitorAttachmentTotalSizeLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor attachment total size limit: %s", visitorAttachmentTotalSizeLimitStr)
|
||||
}
|
||||
visitorAttachmentDailyBandwidthLimit, err := util.ParseSize(visitorAttachmentDailyBandwidthLimitStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor attachment daily bandwidth limit: %s", visitorAttachmentDailyBandwidthLimitStr)
|
||||
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
|
||||
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
|
||||
}
|
||||
|
||||
// Check values
|
||||
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
|
||||
return errors.New("if set, FCM key file must exist")
|
||||
|
@ -213,10 +274,15 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
||||
} else if attachmentCacheDir != "" && baseURL == "" {
|
||||
return errors.New("if attachment-cache-dir is set, base-url must also be set")
|
||||
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||
return errors.New("if set, base-url must start with http:// or https://")
|
||||
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
||||
return errors.New("if set, base-url must not end with a slash (/)")
|
||||
} else if baseURL != "" {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("if set, base-url must be a valid URL, e.g. https://ntfy.mydomain.com: %v", err)
|
||||
} else if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return errors.New("if set, base-url must be a valid URL starting with http:// or https://, e.g. https://ntfy.mydomain.com")
|
||||
} else if u.Path != "" {
|
||||
return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path)
|
||||
}
|
||||
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
||||
|
@ -233,6 +299,11 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
||||
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
|
||||
return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set")
|
||||
} else if messageSizeLimit > server.DefaultMessageSizeLimit {
|
||||
log.Warn("message-size-limit is greater than 4K, this is not recommended and largely untested, and may lead to issues with some clients")
|
||||
if messageSizeLimit > 5*1024*1024 {
|
||||
return errors.New("message-size-limit cannot be higher than 5M")
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
|
@ -257,26 +328,6 @@ func execServe(c *cli.Context) error {
|
|||
listenHTTP = ""
|
||||
}
|
||||
|
||||
// Convert sizes to bytes
|
||||
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
|
||||
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
|
||||
}
|
||||
|
||||
// Resolve hosts
|
||||
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
|
||||
for _, host := range visitorRequestLimitExemptHosts {
|
||||
|
@ -337,6 +388,8 @@ func execServe(c *cli.Context) error {
|
|||
conf.TwilioAuthToken = twilioAuthToken
|
||||
conf.TwilioPhoneNumber = twilioPhoneNumber
|
||||
conf.TwilioVerifyService = twilioVerifyService
|
||||
conf.MessageSizeLimit = int(messageSizeLimit)
|
||||
conf.MessageDelayMax = messageDelayLimit
|
||||
conf.TotalTopicLimit = totalTopicLimit
|
||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
||||
|
@ -379,17 +432,6 @@ func execServe(c *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseSize(s string, defaultValue int64) (v int64, err error) {
|
||||
if s == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
v, err = util.ParseSize(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func sigHandlerConfigReload(config string) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGHUP)
|
||||
|
|
|
@ -366,9 +366,9 @@ func printTier(c *cli.Context, tier *user.Tier) {
|
|||
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSizeHuman(tier.AttachmentTotalSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSizeHuman(tier.AttachmentBandwidthLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
|
||||
}
|
||||
|
|
|
@ -404,10 +404,10 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
|
|||
```
|
||||
|
||||
### Example: UnifiedPush
|
||||
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
|
||||
has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.
|
||||
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/developers/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
|
||||
has anonymous write access to the [topic](https://unifiedpush.org/developers/spec/definitions/#endpoint) used for push messages.
|
||||
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the
|
||||
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
|
||||
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users-acl)** for more details.
|
||||
|
||||
To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either
|
||||
allow anonymous write access for the entire prefix or explicitly per topic:
|
||||
|
@ -995,6 +995,15 @@ are the easiest), and then configure the following options:
|
|||
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.
|
||||
|
||||
## Message limits
|
||||
There are a few message limits that you can configure:
|
||||
|
||||
* `message-size-limit` defines the max size of a message body. Please note message sizes >4K are **not recommended,
|
||||
and largely untested**. The Android/iOS and other clients may not work, or work properly. If FCM and/or APNS is used,
|
||||
the limit should stay 4K, because their limits are around that size. If you increase this size limit regardless,
|
||||
FCM and APNS will NOT work for large messages.
|
||||
* `message-delay-limit` defines the max delay of a message when using the "Delay" header and [scheduled delivery](publish.md#scheduled-delivery).
|
||||
|
||||
## Rate limiting
|
||||
!!! info
|
||||
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
||||
|
@ -1078,20 +1087,23 @@ By default, ntfy puts almost all rate limits on the message publisher, e.g. numb
|
|||
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
|
||||
of a topic's subscriber, instead of the limits of the publisher.**
|
||||
|
||||
If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
|
||||
to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
|
||||
publishers (e.g. Matrix/Mastodon servers) are allowed to send.
|
||||
If subscriber-based rate limiting is enabled, **messages published on UnifiedPush topics** (topics starting with `up`, e.g. `up123456789012`)
|
||||
will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
|
||||
|
||||
Once enabled, a client may send a `Rate-Topics: <topic1>,<topic2>,...` header when subscribing to topics via
|
||||
HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
|
||||
to use when publishing on this topic. Note that setting the rate visitor requires **read-write permission** on the topic.
|
||||
Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
|
||||
a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
|
||||
requires **read-write permission** on the topic.
|
||||
|
||||
UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
|
||||
If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
|
||||
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
|
||||
`visitor-message-daily-limit`.
|
||||
|
||||
To enable subscriber-based rate limiting, set `visitor-subscriber-rate-limiting: true`.
|
||||
|
||||
!!! info
|
||||
Due to a [denial-of-service issue](https://github.com/binwiederhier/ntfy/issues/1048), support for the `Rate-Topics`
|
||||
header was removed entirely. This is unfortunate, but subscriber-based rate limiting will still work for `up*` topics.
|
||||
|
||||
## Tuning for scale
|
||||
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
|
||||
if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
|
||||
|
@ -1388,6 +1400,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `twilio-verify-service` | `NTFY_TWILIO_VERIFY_SERVICE` | *string* | - | Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 |
|
||||
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
|
||||
| `manager-interval` | `NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
||||
| `message-size-limit` | `NTFY_MESSAGE_SIZE_LIMIT` | *size* | 4K | The size limit for the message body. Please note that this is largely untested, and that FCM/APNS have limits around 4KB. If you increase this size limit, FCM and APNS will NOT work for large messages. |
|
||||
| `message-delay-limit` | `NTFY_MESSAGE_DELAY_LIMIT` | *duration* | 3d | Amount of time a message can be [scheduled](publish.md#scheduled-delivery) into the future when using the `Delay` header |
|
||||
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
||||
| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers |
|
||||
| `upstream-access-token` | `NTFY_UPSTREAM_ACCESS_TOKEN` | *string* | `tk_zyYLYj...` | Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth |
|
||||
|
@ -1414,7 +1428,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
|
||||
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
|
||||
|
||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||
The format for a *duration* is: `<number>(smhd)`, e.g. 30s, 20m, 1h or 3d.
|
||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||
|
||||
## Command line options
|
||||
|
@ -1446,7 +1460,7 @@ OPTIONS:
|
|||
--log-level-overrides value, --log_level_overrides value [ --log-level-overrides value, --log_level_overrides value ] set log level overrides [$NTFY_LOG_LEVEL_OVERRIDES]
|
||||
--log-format value, --log_format value set log format (default: "text") [$NTFY_LOG_FORMAT]
|
||||
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
|
||||
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
||||
--config value, -c value config file (default: "/etc/ntfy/server.yml") [$NTFY_CONFIG_FILE]
|
||||
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
||||
--listen-http value, --listen_http value, -l value ip:port used as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
||||
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
||||
|
@ -1456,19 +1470,19 @@ OPTIONS:
|
|||
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
|
||||
--firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
|
||||
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
||||
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
||||
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: "12h") [$NTFY_CACHE_DURATION]
|
||||
--cache-batch-size value, --cache_batch_size value max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
|
||||
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]
|
||||
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: "0s") [$NTFY_CACHE_BATCH_TIMEOUT]
|
||||
--cache-startup-queries value, --cache_startup_queries value queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
|
||||
--auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
|
||||
--auth-startup-queries value, --auth_startup_queries value queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES]
|
||||
--auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
|
||||
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
|
||||
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
|
||||
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
|
||||
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
|
||||
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
||||
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: "5G") [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: "15M") [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
|
||||
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: "3h") [$NTFY_ATTACHMENT_EXPIRY_DURATION]
|
||||
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: "45s") [$NTFY_KEEPALIVE_INTERVAL]
|
||||
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: "1m") [$NTFY_MANAGER_INTERVAL]
|
||||
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
|
||||
--web-root value, --web_root value sets root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
|
||||
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
||||
|
@ -1487,16 +1501,18 @@ OPTIONS:
|
|||
--twilio-auth-token value, --twilio_auth_token value Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
|
||||
--twilio-phone-number value, --twilio_phone_number value Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
|
||||
--twilio-verify-service value, --twilio_verify_service value Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
|
||||
--message-size-limit value, --message_size_limit value size limit for the message (see docs for limitations) (default: "4K") [$NTFY_MESSAGE_SIZE_LIMIT]
|
||||
--message-delay-limit value, --message_delay_limit value max duration a message can be scheduled into the future (default: "3d") [$NTFY_MESSAGE_DELAY_LIMIT]
|
||||
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
||||
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
||||
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
|
||||
--visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
|
||||
--visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
|
||||
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
||||
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: "5s") [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
||||
--visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
|
||||
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
|
||||
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
||||
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: "1h") [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
|
||||
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
||||
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
|
||||
|
@ -1509,6 +1525,6 @@ OPTIONS:
|
|||
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
|
||||
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
|
||||
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
|
||||
--web-push-startup-queries value, --web_push_startup-queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
||||
--web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
||||
--help, -h show help
|
||||
```
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Deprecation notices
|
||||
# Deprecations and breaking changes
|
||||
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
|
||||
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
|
||||
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
|
||||
|
|
|
@ -363,7 +363,7 @@ To build your own version with Firebase, you must:
|
|||
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
|
||||
* Then run:
|
||||
```
|
||||
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
|
||||
# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
|
||||
./gradlew assemblePlayRelease
|
||||
|
||||
# To build a bundle .aab (app/play/release/*.aab)
|
||||
|
|
|
@ -162,14 +162,23 @@ services:
|
|||
image: containrrr/watchtower
|
||||
environment:
|
||||
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
||||
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
|
||||
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
|
||||
```
|
||||
|
||||
The environment variable `WATCHTOWER_NOTIFICATION_SKIP_TITLE` is required to prevent Watchtower from [replacing the `title` query parameter](https://containrrr.dev/watchtower/notifications/#settings). If omitted, the provided notification title will not be used.
|
||||
|
||||
Or, if you only want to send notifications using shoutrrr:
|
||||
```
|
||||
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
||||
```
|
||||
|
||||
Authentication tokens are also supported via the generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
|
||||
|
||||
```
|
||||
generic+https://DOMAIN/TOPIC?@authorization=Bearer+TOKEN`
|
||||
```
|
||||
|
||||
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
||||
|
||||
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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'))
|
||||
|
||||
def on_post_build(config, **kwargs):
|
||||
site_dir = config["site_dir"]
|
||||
shutil.copytree("docs/static/fonts", os.path.join(site_dir, "get"))
|
||||
|
|
|
@ -738,9 +738,8 @@ Usage is pretty straight forward. You can set the delivery time using the `X-Del
|
|||
`3h`, `2 days`), or a natural language time string (e.g. `10am`, `8:30pm`, `tomorrow, 3pm`, `Tuesday, 7am`,
|
||||
[and more](https://github.com/olebedev/when)).
|
||||
|
||||
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can currently
|
||||
not be configured otherwise ([let me know](https://github.com/binwiederhier/ntfy/issues) if you'd like to change
|
||||
these limits).
|
||||
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can be configured
|
||||
with the `message-delay-limit` option).
|
||||
|
||||
For the purposes of [message caching](config.md#message-cache), scheduled messages are kept in the cache until 12 hours
|
||||
after they were delivered (or whatever the server-side cache duration is set to). For instance, if a message is scheduled
|
||||
|
|
|
@ -13,7 +13,7 @@ Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and
|
|||
|
||||
* UI not updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)/[#402](https://github.com/binwiederhier/ntfy/issues/402), thanks to [@tcaputi](https://github.com/tcaputi))
|
||||
|
||||
### ntfy server v2.8.0
|
||||
## ntfy server v2.8.0
|
||||
Released November 19, 2023
|
||||
|
||||
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the `Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally), web app crash fixes
|
||||
|
@ -1313,6 +1313,27 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||
|
||||
## Not released yet
|
||||
|
||||
### ntfy server v2.9.0
|
||||
|
||||
!!! info
|
||||
**Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used.
|
||||
|
||||
**Features:**
|
||||
|
||||
* Support for larger message delays with `message-delay-limit` (see [message limits](config.md#message-limits), [#1050](https://github.com/binwiederhier/ntfy/pull/1050)/[#1019](https://github.com/binwiederhier/ntfy/issues/1019), thanks to [@MrChadMWood](https://github.com/MrChadMWood) for reporting)
|
||||
* Support for larger message body sizes with `message-size-limit` (use at your own risk, see [message limits](config.md#message-limits), [#836](https://github.com/binwiederhier/ntfy/pull/836)/[#1050](https://github.com/binwiederhier/ntfy/pull/1050), thanks to [@zhzy0077](https://github.com/zhzy0077) for implementing this, and to [@nkjshlsqja7331](https://github.com/nkjshlsqja7331) for reporting)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Remove `Rate-Topics` header due to DoS security issue if `visitor-subscriber-rate-limiting: true` ([#1048](https://github.com/binwiederhier/ntfy/issues/1048))
|
||||
* Add non-root user to Docker image, ntfy can be run as non-root ([#967](https://github.com/binwiederhier/ntfy/pull/967)/[#966](https://github.com/binwiederhier/ntfy/issues/966), thanks to [@arahja](https://github.com/arahja))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Remove `mkdocs-simple-hooks` ([#1016](https://github.com/binwiederhier/ntfy/pull/1016), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht))
|
||||
* Update Watchtower example ([#1014](https://github.com/binwiederhier/ntfy/pull/1014), thanks to [@lennart-m](https://github.com/lennart-m))
|
||||
* Fix dead links ([#1022](https://github.com/binwiederhier/ntfy/pull/1022), thanks to [@DerRockWolf](https://github.com/DerRockWolf))
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
|
70
go.mod
70
go.mod
|
@ -5,23 +5,23 @@ go 1.21
|
|||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.14.0 // indirect
|
||||
cloud.google.com/go/storage v1.35.1 // indirect
|
||||
cloud.google.com/go/firestore v1.15.0 // indirect
|
||||
cloud.google.com/go/storage v1.39.0 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/emersion/go-smtp v0.18.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.18
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/olebedev/when v1.0.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/crypto v0.15.0
|
||||
golang.org/x/oauth2 v0.14.0 // indirect
|
||||
golang.org/x/sync v0.5.0
|
||||
golang.org/x/term v0.14.0
|
||||
golang.org/x/time v0.4.0
|
||||
google.golang.org/api v0.151.0
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.168.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
|
@ -30,19 +30,19 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi
|
|||
require github.com/pkg/errors v0.9.1 // indirect
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.12.1
|
||||
firebase.google.com/go/v4 v4.13.0
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/stripe/stripe-go/v74 v74.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.10 // indirect
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go v0.112.1 // indirect
|
||||
cloud.google.com/go/compute v1.25.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.5 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.4 // indirect
|
||||
cloud.google.com/go/iam v1.1.6 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.5 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
|
@ -50,35 +50,41 @@ require (
|
|||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.50.0 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
141
go.sum
141
go.sum
|
@ -1,20 +1,20 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
|
||||
cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
|
||||
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||
cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=
|
||||
cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw=
|
||||
cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ=
|
||||
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg=
|
||||
cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI=
|
||||
cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w=
|
||||
cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
|
||||
firebase.google.com/go/v4 v4.12.1 h1:tDNvobifGsx/1HSFLnM0fmNfx/CDZSgsTO2KhZtgpcs=
|
||||
firebase.google.com/go/v4 v4.12.1/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
||||
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
|
||||
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
|
||||
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
|
||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
||||
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
|
||||
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
|
||||
cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA=
|
||||
cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk=
|
||||
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
|
||||
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
|
@ -48,8 +48,15 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
|||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
|
@ -71,8 +78,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
|||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
|
@ -87,12 +94,12 @@ github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3
|
|||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
|
@ -101,10 +108,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
|
||||
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
|
||||
|
@ -113,15 +118,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
|
||||
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
|
@ -138,19 +143,31 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
||||
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
|
@ -169,18 +186,18 @@ golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
|
||||
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -192,14 +209,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
|
||||
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -209,8 +226,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
||||
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
@ -223,8 +240,8 @@ 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-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.151.0 h1:FhfXLO/NFdJIzQtCqjpysWwqKk8AzGWBUhMIx67cVDU=
|
||||
google.golang.org/api v0.151.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibzhxcg=
|
||||
google.golang.org/api v0.168.0 h1:MBRe+Ki4mMN93jhDDbpuRLjRddooArz4FeSObvUMmjY=
|
||||
google.golang.org/api v0.168.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
|
@ -234,19 +251,19 @@ google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7
|
|||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 h1:Fe8QycXyEd9mJgnwB9kmw00WgB43eQ/xYO5C6gceybQ=
|
||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
@ -259,8 +276,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
|||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
@ -65,15 +65,15 @@ markdown_extensions:
|
|||
- md_in_html
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
|
||||
hooks:
|
||||
- docs/hooks.py
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- minify:
|
||||
minify_html: true
|
||||
- mkdocs-simple-hooks:
|
||||
hooks:
|
||||
on_post_build: "docs.hooks:copy_fonts"
|
||||
|
||||
nav:
|
||||
- "Getting started": index.md
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# The documentation uses 'mkdocs', which is written in Python
|
||||
mkdocs-material
|
||||
mkdocs-minify-plugin
|
||||
mkdocs-simple-hooks
|
||||
|
|
|
@ -12,11 +12,12 @@ import (
|
|||
const (
|
||||
DefaultListenHTTP = ":80"
|
||||
DefaultCacheDuration = 12 * time.Hour
|
||||
DefaultCacheBatchTimeout = time.Duration(0)
|
||||
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
||||
DefaultManagerInterval = time.Minute
|
||||
DefaultDelayedSenderInterval = 10 * time.Second
|
||||
DefaultMinDelay = 10 * time.Second
|
||||
DefaultMaxDelay = 3 * 24 * time.Hour
|
||||
DefaultMessageDelayMin = 10 * time.Second
|
||||
DefaultMessageDelayMax = 3 * 24 * time.Hour
|
||||
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery
|
||||
DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
|
||||
DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded"
|
||||
|
@ -34,7 +35,7 @@ const (
|
|||
// - total topic limit: max number of topics overall
|
||||
// - various attachment limits
|
||||
const (
|
||||
DefaultMessageLengthLimit = 4096 // Bytes
|
||||
DefaultMessageSizeLimit = 4096 // Bytes; note that FCM/APNS have a limit of ~4 KB for the entire message
|
||||
DefaultTotalTopicLimit = 15000
|
||||
DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
|
||||
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
|
||||
|
@ -122,9 +123,9 @@ type Config struct {
|
|||
MetricsEnable bool
|
||||
MetricsListenHTTP string
|
||||
ProfileListenHTTP string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
MessageDelayMin time.Duration
|
||||
MessageDelayMax time.Duration
|
||||
MessageSizeLimit int
|
||||
TotalTopicLimit int
|
||||
TotalAttachmentSizeLimit int64
|
||||
VisitorSubscriptionLimit int
|
||||
|
@ -211,9 +212,9 @@ func NewConfig() *Config {
|
|||
TwilioPhoneNumber: "",
|
||||
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
|
||||
TwilioVerifyService: "",
|
||||
MessageLimit: DefaultMessageLengthLimit,
|
||||
MinDelay: DefaultMinDelay,
|
||||
MaxDelay: DefaultMaxDelay,
|
||||
MessageSizeLimit: DefaultMessageSizeLimit,
|
||||
MessageDelayMin: DefaultMessageDelayMin,
|
||||
MessageDelayMax: DefaultMessageDelayMax,
|
||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||
TotalAttachmentSizeLimit: 0,
|
||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||
|
|
|
@ -733,7 +733,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := util.Peek(r.Body, s.config.MessageLimit)
|
||||
body, err := util.Peek(r.Body, s.config.MessageSizeLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -743,8 +743,8 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||
return nil, e.With(t)
|
||||
}
|
||||
if unifiedpush && s.config.VisitorSubscriberRateLimiting && t.RateVisitor() == nil {
|
||||
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting (see
|
||||
// Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove
|
||||
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting.
|
||||
// The 5xx response is because some app servers (in particular Mastodon) will remove
|
||||
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
|
||||
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
|
||||
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
|
||||
|
@ -996,9 +996,9 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||
if err != nil {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
|
||||
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
|
||||
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
|
||||
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
|
||||
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
|
||||
}
|
||||
m.Time = delay.Unix()
|
||||
|
@ -1182,7 +1182,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
|
||||
poll, since, scheduled, filters, err := parseSubscribeParams(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1212,7 +1212,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
|||
}
|
||||
return nil
|
||||
}
|
||||
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
|
||||
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||
|
@ -1278,7 +1278,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
|
||||
poll, since, scheduled, filters, err := parseSubscribeParams(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1364,7 +1364,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
|||
}
|
||||
return conn.WriteJSON(msg)
|
||||
}
|
||||
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
|
||||
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||
|
@ -1397,7 +1397,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
|||
return err
|
||||
}
|
||||
|
||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, rateTopics []string, err error) {
|
||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
|
||||
poll = readBoolParam(r, false, "x-poll", "poll", "po")
|
||||
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
|
||||
since, err = parseSince(r, poll)
|
||||
|
@ -1408,7 +1408,6 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
rateTopics = readCommaSeparatedParam(r, "x-rate-topics", "rate-topics")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1420,9 +1419,8 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
|
|||
// - or the topic is reserved, and v.user is the owner
|
||||
// - or the topic is not reserved, and v.user has write access
|
||||
//
|
||||
// Note: This TEMPORARILY also registers all topics starting with "up" (= UnifiedPush). This is to ease the transition
|
||||
// until the Android app will send the "Rate-Topics" header.
|
||||
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic, rateTopics []string) error {
|
||||
// This only applies to UnifiedPush topics ("up...").
|
||||
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic) error {
|
||||
// Bail out if not enabled
|
||||
if !s.config.VisitorSubscriberRateLimiting {
|
||||
return nil
|
||||
|
@ -1431,7 +1429,7 @@ func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*top
|
|||
// Make a list of topics that we'll actually set the RateVisitor on
|
||||
eligibleRateTopics := make([]*topic, 0)
|
||||
for _, t := range topics {
|
||||
if (strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength) || util.Contains(rateTopics, t.ID) {
|
||||
if strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength {
|
||||
eligibleRateTopics = append(eligibleRateTopics, t)
|
||||
}
|
||||
}
|
||||
|
@ -1756,7 +1754,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
|||
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
|
||||
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageLimit*2, false) // 2x to account for JSON format overhead
|
||||
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageSizeLimit*2, false) // 2x to account for JSON format overhead
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1814,7 +1812,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
|||
|
||||
func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageLimit)
|
||||
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageSizeLimit)
|
||||
if err != nil {
|
||||
logvr(v, r).Tag(tagMatrix).Err(err).Debug("Invalid Matrix request")
|
||||
if e, ok := err.(*errMatrixPushkeyRejected); ok {
|
||||
|
|
|
@ -236,6 +236,16 @@
|
|||
# upstream-base-url:
|
||||
# upstream-access-token:
|
||||
|
||||
# Configures message-specific limits
|
||||
#
|
||||
# - message-size-limit defines the max size of a message body. Please note message sizes >4K are NOT RECOMMENDED,
|
||||
# and largely untested. If FCM and/or APNS is used, the limit should stay 4K, because their limits are around that size.
|
||||
# If you increase this size limit regardless, FCM and APNS will NOT work for large messages.
|
||||
# - message-delay-limit defines the max delay of a message when using the "Delay" header.
|
||||
#
|
||||
# message-size-limit: "4k"
|
||||
# message-delay-limit: "3d"
|
||||
|
||||
# Rate limiting: Total number of topics before the server rejects new topics.
|
||||
#
|
||||
# global-topic-limit: 15000
|
||||
|
@ -277,15 +287,14 @@
|
|||
|
||||
# Rate limiting: Enable subscriber-based rate limiting (mostly used for UnifiedPush)
|
||||
#
|
||||
# If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
|
||||
# to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
|
||||
# publishers (e.g. Matrix/Mastodon servers) are allowed to send.
|
||||
# If subscriber-based rate limiting is enabled, messages published on UnifiedPush topics** (topics starting with "up")
|
||||
# will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
|
||||
#
|
||||
# Once enabled, a client may send a "Rate-Topics: <topic1>,<topic2>,..." header when subscribing to topics via
|
||||
# HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
|
||||
# to use when publishing on this topic. Note: Setting the rate visitor requires READ-WRITE permission on the topic.
|
||||
# Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
|
||||
# a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
|
||||
# requires **read-write permission** on the topic.
|
||||
#
|
||||
# UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
|
||||
# If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
|
||||
# no "rate visitor" has been previously registered. This is to avoid burning the publisher's "visitor-message-daily-limit".
|
||||
#
|
||||
# visitor-subscriber-rate-limiting: false
|
||||
|
|
|
@ -718,11 +718,11 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
|
|||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "starter",
|
||||
MessageLimit: 10,
|
||||
MessageSizeLimit: 10,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "pro",
|
||||
MessageLimit: 20,
|
||||
MessageSizeLimit: 20,
|
||||
}))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "starter"))
|
||||
|
||||
|
|
|
@ -1346,9 +1346,7 @@ func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/up123456789012/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "up123456789012",
|
||||
})
|
||||
response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish message to topic
|
||||
|
@ -1379,9 +1377,7 @@ func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic",
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish message to topic
|
||||
|
@ -1400,9 +1396,7 @@ func TestServer_PublishUnifiedPushText(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic",
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish UnifiedPush text message
|
||||
|
@ -1434,9 +1428,7 @@ func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing.T) {
|
|||
func TestServer_MatrixGateway_Push_Success(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic", // Register first!
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
|
||||
|
@ -2266,16 +2258,14 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
|||
c.VisitorSubscriberRateLimiting = true
|
||||
s := newTestServer(t, c)
|
||||
|
||||
// "Register" visitor 1.2.3.4 to topic "subscriber1topic" as a rate limit visitor
|
||||
// "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor
|
||||
subscriber1Fn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
}
|
||||
rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "subscriber1topic",
|
||||
}, subscriber1Fn)
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
require.Equal(t, "1.2.3.4", s.topics["subscriber1topic"].rateVisitor.ip.String())
|
||||
require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
|
||||
|
||||
// "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name)
|
||||
subscriber2Fn := func(r *http.Request) {
|
||||
|
@ -2289,10 +2279,10 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
|||
// Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the
|
||||
// GET request before is also counted towards the request limiter.
|
||||
for i := 0; i < 2; i++ {
|
||||
rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
|
||||
// Publish another 2 messages to "up012345678912" as visitor 9.9.9.9
|
||||
|
@ -2325,14 +2315,12 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
|||
// Subscriber rate limiting is disabled!
|
||||
|
||||
// Registering visitor 1.2.3.4 to topic has no effect
|
||||
rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "subscriber1topic",
|
||||
}, func(r *http.Request) {
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
require.Nil(t, s.topics["subscriber1topic"].rateVisitor)
|
||||
require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Registering visitor 8.7.7.1 to topic has no effect
|
||||
rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) {
|
||||
|
@ -2342,7 +2330,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
|||
require.Equal(t, "", rr.Body.String())
|
||||
require.Nil(t, s.topics["up012345678912"].rateVisitor)
|
||||
|
||||
// Publish 3 messages to "subscriber1topic" as visitor 9.9.9.9
|
||||
// Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9
|
||||
for i := 0; i < 3; i++ {
|
||||
rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
@ -2415,80 +2403,30 @@ func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
|
|||
subscriberFn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
}
|
||||
rr := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"rate-topics": "mytopic",
|
||||
}, subscriberFn)
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "1.2.3.4", s.topics["mytopic"].rateVisitor.ip.String())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
|
||||
require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Publish message, observe rate visitor tokens being decreased
|
||||
response := request(t, s, "POST", "/mytopic", "some message", nil)
|
||||
response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
|
||||
require.Equal(t, int64(1), s.topics["mytopic"].rateVisitor.messagesLimiter.Value())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
|
||||
require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Expire visitor
|
||||
s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour)
|
||||
s.pruneVisitors()
|
||||
|
||||
// Publish message again, observe that rateVisitor is not used anymore and is reset
|
||||
response = request(t, s, "POST", "/mytopic", "some message", nil)
|
||||
response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
|
||||
require.Nil(t, s.topics["mytopic"].rateVisitor)
|
||||
require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
require.Nil(t, s.visitors["ip:1.2.3.4"])
|
||||
}
|
||||
|
||||
func TestServer_SubscriberRateLimiting_ProtectedTopics(t *testing.T) {
|
||||
c := newTestConfigWithAuthFile(t)
|
||||
c.AuthDefault = user.PermissionDenyAll
|
||||
c.VisitorSubscriberRateLimiting = true
|
||||
s := newTestServer(t, c)
|
||||
|
||||
// Create some ACLs
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "test",
|
||||
MessageLimit: 5,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("ben", "test"))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "announcements", user.PermissionReadWrite))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "public_topic", user.PermissionReadWrite))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
require.Nil(t, s.userManager.AddReservation("phil", "reserved-for-phil", user.PermissionReadWrite))
|
||||
|
||||
// Set rate visitor as user "phil" on topic
|
||||
// - "reserved-for-phil": Allowed, because I am the owner
|
||||
// - "public_topic": Allowed, because it has read-write permissions for everyone
|
||||
// - "announcements": NOT allowed, because it has read-only permissions for everyone
|
||||
rr := request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
"Rate-Topics": "reserved-for-phil,public_topic,announcements",
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
|
||||
require.Equal(t, "phil", s.topics["public_topic"].rateVisitor.user.Name)
|
||||
require.Nil(t, s.topics["announcements"].rateVisitor)
|
||||
|
||||
// Set rate visitor as user "ben" on topic
|
||||
// - "reserved-for-phil": NOT allowed, because I am not the owner
|
||||
// - "public_topic": Allowed, because it has read-write permissions for everyone
|
||||
// - "announcements": Allowed, because I have read-write permissions
|
||||
rr = request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
"Rate-Topics": "reserved-for-phil,public_topic,announcements",
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
|
||||
require.Equal(t, "ben", s.topics["public_topic"].rateVisitor.user.Name)
|
||||
require.Equal(t, "ben", s.topics["announcements"].rateVisitor.user.Name)
|
||||
}
|
||||
|
||||
func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *testing.T) {
|
||||
c := newTestConfigWithAuthFile(t)
|
||||
c.AuthDefault = user.PermissionReadWrite
|
||||
|
|
|
@ -150,8 +150,8 @@ func (s *smtpSession) Data(r io.Reader) error {
|
|||
return err
|
||||
}
|
||||
body = strings.TrimSpace(body)
|
||||
if len(body) > conf.MessageLimit {
|
||||
body = body[:conf.MessageLimit]
|
||||
if len(body) > conf.MessageSizeLimit {
|
||||
body = body[:conf.MessageSizeLimit]
|
||||
}
|
||||
m := newDefaultMessage(s.topic, body)
|
||||
subject := strings.TrimSpace(msg.Header.Get("Subject"))
|
||||
|
|
|
@ -30,10 +30,10 @@ const (
|
|||
visitorDefaultCallsLimit = int64(0)
|
||||
)
|
||||
|
||||
// Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter
|
||||
// Constants used to convert a tier-user's MessageSizeLimit (see user.Tier) into adequate request limiter
|
||||
// values (token bucket). This is only used to increase the values in server.yml, never decrease them.
|
||||
//
|
||||
// Example: Assuming a user.Tier's MessageLimit is 10,000:
|
||||
// Example: Assuming a user.Tier's MessageSizeLimit is 10,000:
|
||||
// - the allowed burst is 500 (= 10,000 * 5%), which is < 1000 (the max)
|
||||
// - the replenish rate is 2 * 10,000 / 24 hours
|
||||
const (
|
||||
|
|
40
util/time.go
40
util/time.go
|
@ -10,8 +10,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
errUnparsableTime = errors.New("unable to parse time")
|
||||
durationStrRegex = regexp.MustCompile(`(?i)^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$`)
|
||||
errInvalidDuration = errors.New("unable to parse duration")
|
||||
durationStrRegex = regexp.MustCompile(`(?i)^(\d+)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)$`)
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -51,7 +51,7 @@ func ParseFutureTime(s string, now time.Time) (time.Time, error) {
|
|||
if err == nil {
|
||||
return t, nil
|
||||
}
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
}
|
||||
|
||||
// ParseDuration is like time.ParseDuration, except that it also understands days (d), which
|
||||
|
@ -65,7 +65,7 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||
if matches != nil {
|
||||
number, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return 0, errUnparsableTime
|
||||
return 0, errInvalidDuration
|
||||
}
|
||||
switch unit := matches[2][0:1]; unit {
|
||||
case "d":
|
||||
|
@ -77,10 +77,28 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||
case "s":
|
||||
return time.Duration(number) * time.Second, nil
|
||||
default:
|
||||
return 0, errUnparsableTime
|
||||
return 0, errInvalidDuration
|
||||
}
|
||||
}
|
||||
return 0, errUnparsableTime
|
||||
return 0, errInvalidDuration
|
||||
}
|
||||
|
||||
// FormatDuration formats a time.Duration into a human-readable string, e.g. "2d", "20h", "30m", "40s".
|
||||
// It rounds to the largest unit that is not zero, thereby effectively rounding down.
|
||||
func FormatDuration(d time.Duration) string {
|
||||
if d >= 24*time.Hour {
|
||||
return strconv.Itoa(int(d/(24*time.Hour))) + "d"
|
||||
}
|
||||
if d >= time.Hour {
|
||||
return strconv.Itoa(int(d/time.Hour)) + "h"
|
||||
}
|
||||
if d >= time.Minute {
|
||||
return strconv.Itoa(int(d/time.Minute)) + "m"
|
||||
}
|
||||
if d >= time.Second {
|
||||
return strconv.Itoa(int(d/time.Second)) + "s"
|
||||
}
|
||||
return "0s"
|
||||
}
|
||||
|
||||
func parseFromDuration(s string, now time.Time) (time.Time, error) {
|
||||
|
@ -88,7 +106,7 @@ func parseFromDuration(s string, now time.Time) (time.Time, error) {
|
|||
if err == nil {
|
||||
return now.Add(d), nil
|
||||
}
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
}
|
||||
|
||||
func parseUnixTime(s string, now time.Time) (time.Time, error) {
|
||||
|
@ -96,7 +114,7 @@ func parseUnixTime(s string, now time.Time) (time.Time, error) {
|
|||
if err != nil {
|
||||
return time.Time{}, err
|
||||
} else if int64(t) < now.Unix() {
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
}
|
||||
return time.Unix(int64(t), 0).UTC(), nil
|
||||
}
|
||||
|
@ -104,7 +122,7 @@ func parseUnixTime(s string, now time.Time) (time.Time, error) {
|
|||
func parseNaturalTime(s string, now time.Time) (time.Time, error) {
|
||||
r, err := when.EN.Parse(s, now) // returns "nil, nil" if no matches!
|
||||
if err != nil || r == nil {
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
} else if r.Time.After(now) {
|
||||
return r.Time, nil
|
||||
}
|
||||
|
@ -112,9 +130,9 @@ func parseNaturalTime(s string, now time.Time) (time.Time, error) {
|
|||
// simply append "tomorrow, " to it.
|
||||
r, err = when.EN.Parse("tomorrow, "+s, now) // returns "nil, nil" if no matches!
|
||||
if err != nil || r == nil {
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
} else if r.Time.After(now) {
|
||||
return r.Time, nil
|
||||
}
|
||||
return time.Time{}, errUnparsableTime
|
||||
return time.Time{}, errInvalidDuration
|
||||
}
|
||||
|
|
|
@ -92,3 +92,27 @@ func TestParseDuration(t *testing.T) {
|
|||
require.Nil(t, err)
|
||||
require.Equal(t, time.Duration(0), d)
|
||||
}
|
||||
|
||||
func TestFormatDuration(t *testing.T) {
|
||||
values := []struct {
|
||||
duration time.Duration
|
||||
expected string
|
||||
}{
|
||||
{24 * time.Second, "24s"},
|
||||
{56 * time.Minute, "56m"},
|
||||
{time.Hour, "1h"},
|
||||
{2 * time.Hour, "2h"},
|
||||
{24 * time.Hour, "1d"},
|
||||
{3 * 24 * time.Hour, "3d"},
|
||||
}
|
||||
for _, value := range values {
|
||||
require.Equal(t, value.expected, FormatDuration(value.duration))
|
||||
d, err := ParseDuration(FormatDuration(value.duration))
|
||||
require.Nil(t, err)
|
||||
require.Equalf(t, value.duration, d, "duration does not match: %v != %v", value.duration, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDuration_Rounded(t *testing.T) {
|
||||
require.Equal(t, "1d", FormatDuration(47*time.Hour))
|
||||
}
|
||||
|
|
22
util/util.go
22
util/util.go
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
@ -215,6 +216,8 @@ func ParseSize(s string) (int64, error) {
|
|||
return -1, fmt.Errorf("cannot convert number %s", matches[1])
|
||||
}
|
||||
switch strings.ToUpper(matches[2]) {
|
||||
case "T":
|
||||
return int64(value) * 1024 * 1024 * 1024 * 1024, nil
|
||||
case "G":
|
||||
return int64(value) * 1024 * 1024 * 1024, nil
|
||||
case "M":
|
||||
|
@ -226,8 +229,23 @@ func ParseSize(s string) (int64, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// FormatSize formats bytes into a human-readable notation, e.g. 2.1 MB
|
||||
// FormatSize formats the size in a way that it can be parsed by ParseSize.
|
||||
// It does not include decimal places. Uneven sizes are rounded down.
|
||||
func FormatSize(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%d%c", int(math.Floor(float64(b)/float64(div))), "KMGT"[exp])
|
||||
}
|
||||
|
||||
// FormatSizeHuman formats bytes into a human-readable notation, e.g. 2.1 MB
|
||||
func FormatSizeHuman(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d bytes", b)
|
||||
|
@ -237,7 +255,7 @@ func FormatSize(b int64) string {
|
|||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGT"[exp])
|
||||
}
|
||||
|
||||
// ReadPassword will read a password from STDIN. If the terminal supports it, it will not print the
|
||||
|
|
|
@ -110,33 +110,47 @@ func TestShortTopicURL(t *testing.T) {
|
|||
|
||||
func TestParseSize_10GSuccess(t *testing.T) {
|
||||
s, err := ParseSize("10G")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(10*1024*1024*1024), s)
|
||||
}
|
||||
|
||||
func TestParseSize_10MUpperCaseSuccess(t *testing.T) {
|
||||
s, err := ParseSize("10M")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(10*1024*1024), s)
|
||||
}
|
||||
|
||||
func TestParseSize_10kLowerCaseSuccess(t *testing.T) {
|
||||
s, err := ParseSize("10k")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, int64(10*1024), s)
|
||||
}
|
||||
|
||||
func TestParseSize_FailureInvalid(t *testing.T) {
|
||||
_, err := ParseSize("not a size")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, but got none")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFormatSize(t *testing.T) {
|
||||
values := []struct {
|
||||
size int64
|
||||
expected string
|
||||
}{
|
||||
{10, "10"},
|
||||
{10 * 1024, "10K"},
|
||||
{10 * 1024 * 1024, "10M"},
|
||||
{10 * 1024 * 1024 * 1024, "10G"},
|
||||
}
|
||||
for _, value := range values {
|
||||
require.Equal(t, value.expected, FormatSize(value.size))
|
||||
s, err := ParseSize(FormatSize(value.size))
|
||||
require.Nil(t, err)
|
||||
require.Equalf(t, value.size, s, "size does not match: %d != %d", value.size, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSize_Rounded(t *testing.T) {
|
||||
require.Equal(t, "10K", FormatSize(10*1024+999))
|
||||
}
|
||||
|
||||
func TestSplitKV(t *testing.T) {
|
||||
|
|
2610
web/package-lock.json
generated
2610
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -55,14 +55,14 @@
|
|||
"notifications_attachment_open_title": "Към {{url}}",
|
||||
"notifications_attachment_copy_url_button": "Копиране на адреса",
|
||||
"notifications_attachment_open_button": "Отваряне на прикачения файл",
|
||||
"notifications_attachment_link_expires": "препратката изтича на {{date}}",
|
||||
"notifications_attachment_link_expires": "давността на препратката изтича на {{date}}",
|
||||
"notifications_actions_open_url_title": "Към {{url}}",
|
||||
"notifications_click_copy_url_button": "Копиране на препратка",
|
||||
"notifications_click_open_button": "Отваряне",
|
||||
"notifications_click_copy_url_title": "Копиране на препратката в междинната памет",
|
||||
"notifications_none_for_topic_title": "Липсват известия в темата.",
|
||||
"notifications_none_for_topic_title": "Темата е все още празна",
|
||||
"notifications_none_for_any_title": "Липсват известия.",
|
||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса й.",
|
||||
"notifications_none_for_topic_description": "За да изпратите известия в тази тема направете заявка чрез методите PUT или POST към адреса ѝ.",
|
||||
"notifications_none_for_any_description": "За да изпратите известия в тема направете заявка чрез методите PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
|
||||
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като направите заявка чрез методите PUT или POST ще ги получите тук.",
|
||||
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
|
||||
|
@ -85,7 +85,7 @@
|
|||
"publish_dialog_title_label": "Заглавие",
|
||||
"publish_dialog_priority_label": "Приоритет",
|
||||
"publish_dialog_click_placeholder": "Адрес, който се отваря при щракване върху известието",
|
||||
"publish_dialog_email_placeholder": "Поща, на която да се препрати известието, напр. phil@example.com",
|
||||
"publish_dialog_email_placeholder": "Адрес, към който да бъдат препращани известия, напр. phil@example.com",
|
||||
"publish_dialog_attach_label": "Адрес на прикачения файл",
|
||||
"publish_dialog_filename_placeholder": "Име на прикачения файл",
|
||||
"publish_dialog_attach_placeholder": "Прикачете файл от адрес, напр. https://f-droid.org/F-Droid.apk",
|
||||
|
@ -155,7 +155,7 @@
|
|||
"notifications_actions_not_supported": "Действието не се поддържа от приложението за интернет",
|
||||
"action_bar_show_menu": "Показване на менюто",
|
||||
"action_bar_logo_alt": "Логотип на ntfy",
|
||||
"action_bar_toggle_mute": "Заглушаване или пускне на известията",
|
||||
"action_bar_toggle_mute": "Заглушаване или пускане на известията",
|
||||
"action_bar_toggle_action_menu": "Отваряне или затваряне на менюто с действията",
|
||||
"nav_button_muted": "Известията са заглушени",
|
||||
"notifications_list": "Списък с известия",
|
||||
|
@ -257,7 +257,7 @@
|
|||
"account_tokens_dialog_button_cancel": "Отказ",
|
||||
"account_delete_title": "Премахване на профила",
|
||||
"account_upgrade_dialog_title": "Промяна нивото на профила",
|
||||
"account_usage_emails_title": "Изпратени съобщения",
|
||||
"account_usage_emails_title": "Изпратени електронни писма",
|
||||
"account_usage_reservations_title": "Резервирани теми",
|
||||
"account_usage_reservations_none": "Няма резервирани теми",
|
||||
"account_usage_cannot_create_portal_session": "Порталът за разплащане не може да бъде отворен",
|
||||
|
@ -332,6 +332,53 @@
|
|||
"account_upgrade_dialog_tier_price_per_month": "на месец",
|
||||
"account_upgrade_dialog_button_pay_now": "Плащане и абониране",
|
||||
"account_upgrade_dialog_tier_selected_label": "Избрано",
|
||||
"account_upgrade_dialog_button_update_subscription": "Премяна на абонамент",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Избраното ниво разрешава по-малко резервирани теми, от колкото текущото. Преди промяна на нивото <strong>изтрийте най-малко {{count}} резервирани теми</strong>. Можете да премахвате теми в <Link>Настройки</Link>."
|
||||
"account_upgrade_dialog_button_update_subscription": "Промяна на абонамент",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Избраното ниво разрешава по-малко резервирани теми, от колкото текущото. Преди промяна на нивото <strong>изтрийте най-малко {{count}} резервирани теми</strong>. Можете да премахвате теми в <Link>Настройки</Link>.",
|
||||
"account_tokens_table_expires_header": "Изтича",
|
||||
"account_tokens_table_never_expires": "Никога",
|
||||
"prefs_reservations_title": "Резервирани теми",
|
||||
"prefs_reservations_table_click_to_subscribe": "Докоснете, за да се абонирате",
|
||||
"prefs_reservations_dialog_title_delete": "Премахване на резервирането",
|
||||
"prefs_reservations_table_everyone_read_only": "Аз мога да публикувам и да се абонирам, всички останали могат да се абонират",
|
||||
"prefs_reservations_table_not_subscribed": "Без абонамент",
|
||||
"account_tokens_table_token_header": "Код за достъп",
|
||||
"account_tokens_table_create_token_button": "Създаване на код за достъп",
|
||||
"account_tokens_dialog_expires_x_days": "Кодът за достъп изтича след {{days}} дена",
|
||||
"account_tokens_dialog_expires_never": "Кодът за достъп не изтича",
|
||||
"account_tokens_delete_dialog_title": "Премахване на код за достъп",
|
||||
"prefs_reservations_limit_reached": "Достигнахте ограничението за брой резервирани теми.",
|
||||
"prefs_reservations_add_button": "Добавяне на тема",
|
||||
"prefs_reservations_delete_button": "Нулиране на правата за достъп",
|
||||
"prefs_reservations_table": "Списък с резервирани теми",
|
||||
"prefs_reservations_dialog_title_add": "Резервиране на тема",
|
||||
"prefs_reservations_dialog_title_edit": "Променяне на резервирана тема",
|
||||
"account_tokens_table_current_session": "Текущ сеанс на четеца",
|
||||
"account_tokens_table_copied_to_clipboard": "Кодът за достъп е копиран",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Не можете да променяте или премахвате кода за достъп на текущия сеанс",
|
||||
"account_tokens_table_last_origin_tooltip": "От адрес по IP {{ip}}, щракнете за подробности",
|
||||
"account_tokens_dialog_title_create": "Създаване на код за достъп",
|
||||
"account_tokens_dialog_title_edit": "Променяне на код за достъп",
|
||||
"account_tokens_dialog_title_delete": "Премахване на код за достъп",
|
||||
"account_tokens_dialog_label": "Етикет, напр. Известия от Radarr",
|
||||
"account_tokens_dialog_button_create": "Създаване на код за достъп",
|
||||
"account_tokens_dialog_button_update": "Променяне на код за достъп",
|
||||
"account_tokens_dialog_expires_label": "Кодът за достъп изтича след",
|
||||
"account_tokens_dialog_expires_x_hours": "Кодът за достъп изтича след {{hours}} часа",
|
||||
"account_tokens_dialog_expires_unchanged": "Без промяна на давността",
|
||||
"account_tokens_delete_dialog_submit_button": "Безвъзвратно премахване на код за достъп",
|
||||
"prefs_users_description_no_sync": "Потребителите и паролите не се синхронизират заедно с профила.",
|
||||
"prefs_users_table_cannot_delete_or_edit": "Влезлият потребител не може да бъде премахнат",
|
||||
"prefs_reservations_table_everyone_deny_all": "Само аз мога да публикувам и да се абонирам",
|
||||
"prefs_reservations_table_everyone_write_only": "Аз мога да публикувам и да се абонирам, всички останали могат да публикуват",
|
||||
"prefs_reservations_table_everyone_read_write": "Всички могат да публикуват и да се абонират",
|
||||
"reservation_delete_dialog_submit_button": "Премахване на резервирането",
|
||||
"account_tokens_description": "Използвайте код за достъп когато публикувате или се абонирате през ППИ на ntfy, за да не се налага да изпращате потребителско име и парола. Прочетете <Link>документацията</Link> за повече информация.",
|
||||
"account_tokens_delete_dialog_description": "Преди да премахвате код за достъп се уверете, че не се използва от приложения или скриптове. <strong>Действието е необратимо.</strong>",
|
||||
"prefs_reservations_dialog_description": "Резервирането ви осигурява собственост върху темата и ви дава възможност да определяте права за достъп от други потребители.",
|
||||
"reservation_delete_dialog_action_keep_title": "Пазене на съобщения и прикачени файлове",
|
||||
"reservation_delete_dialog_action_keep_description": "Съобщенията и прикачените файлове, които са във временната памет на сървъра ще бъдат достъпни за всеки, който знае името на темата.",
|
||||
"reservation_delete_dialog_action_delete_title": "Премахване на съобщения и прикачени файлове",
|
||||
"reservation_delete_dialog_action_delete_description": "Съобщенията и прикачените файлове, които са във временната памет ще бъдат премахнати. Действието е необратимо.",
|
||||
"prefs_reservations_description": "Тук можете да резервирате тема за собствено ползване. Резервирането ви осигурява собственост върху темата и ви дава възможност да определяте права за достъп от други потребители.",
|
||||
"reservation_delete_dialog_description": "С премахването на резервирането вие се отказвате от собствеността върху темата и давате възможност друг потребител да я резервира. Можете да оставите или да премахнете съществуващите съобщения и прикачени файлове."
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"action_bar_sign_up": "Opret konto",
|
||||
"message_bar_type_message": "Skriv en besked her",
|
||||
"nav_button_settings": "Indstillinger",
|
||||
"message_bar_publish": "Offentliggør besked",
|
||||
"message_bar_publish": "Udgiv besked",
|
||||
"nav_topics_title": "Tilmeldte emner",
|
||||
"nav_button_all_notifications": "Alle notifikationer",
|
||||
"nav_button_connecting": "forbinder",
|
||||
|
@ -103,7 +103,7 @@
|
|||
"account_basics_tier_free": "Gratis",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(intet niveau)",
|
||||
"account_basics_tier_admin_suffix_with_tier": "(med {{tier}}} niveau)",
|
||||
"account_usage_messages_title": "Offentliggjorte meddelelser",
|
||||
"account_usage_messages_title": "Udgivne beskeder",
|
||||
"account_delete_dialog_button_submit": "Slet konto permanent",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pr. fil",
|
||||
"account_upgrade_dialog_button_redirect_signup": "Tilmeld dig nu",
|
||||
|
@ -279,5 +279,106 @@
|
|||
"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>."
|
||||
"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>.",
|
||||
"account_upgrade_dialog_tier_features_no_calls": "Ingen telefonopkald",
|
||||
"account_upgrade_dialog_billing_contact_email": "For faktureringsspørgsmål bedes du <Link>kontakte os</Link> direkte.",
|
||||
"account_basics_tier_interval_monthly": "månedlig",
|
||||
"publish_dialog_checkbox_publish_another": "Udgiv en anden",
|
||||
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} daglige telefonopkald",
|
||||
"publish_dialog_filename_placeholder": "Vedhæftet filnavn",
|
||||
"prefs_users_description": "Tilføj/fjern brugere til dine beskyttede emner her. Vær opmærksom på, at brugernavn og adgangskode er gemt i browserens lokale lager.",
|
||||
"account_basics_phone_numbers_dialog_number_label": "Telefonnummer",
|
||||
"subscribe_dialog_subscribe_description": "Emner kan ikke beskyttes med adgangskode, så vælg et navn, der ikke er let at gætte. Når du har abonneret, kan du PUT/POST notifikationer.",
|
||||
"account_basics_phone_numbers_dialog_check_verification_button": "Bekræft kode",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save_up_to": "spar op til {{discount}}%",
|
||||
"account_upgrade_dialog_proration_info": "<strong>Proration</strong>: Når du opgraderer mellem betalte planer, vil prisforskellen blive <strong>opkrævet med det samme</strong>. Ved nedgradering til et lavere niveau, vil saldoen blive brugt til at betale for fremtidige faktureringsperioder.",
|
||||
"account_usage_attachment_storage_title": "opbevaring af vedhæftede filer",
|
||||
"message_bar_error_publishing": "Der opstod en fejl under udgivelse af meddelelse",
|
||||
"publish_dialog_chip_delay_label": "Forsinke leveringen",
|
||||
"prefs_reservations_table_not_subscribed": "Ikke abonneret",
|
||||
"account_upgrade_dialog_tier_features_calls_other": "{{calls}} daglige telefonopkald",
|
||||
"account_basics_phone_numbers_dialog_verify_button_sms": "Send SMS",
|
||||
"prefs_reservations_table_everyone_read_only": "Jeg kan udgive og abonnere, alle kan abonnere",
|
||||
"prefs_reservations_table_everyone_deny_all": "Kun jeg kan udgive og abonnere",
|
||||
"publish_dialog_chip_topic_label": "Skift emne",
|
||||
"account_basics_phone_numbers_dialog_description": "For at bruge opkaldsmeddelelsesfunktionen skal du tilføje og bekræfte mindst ét telefonnummer. Bekræftelse kan gøres via SMS eller et telefonopkald.",
|
||||
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} reserveret emne",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Ingen reserverede emner",
|
||||
"publish_dialog_base_url_label": "Tjeneste-URL",
|
||||
"prefs_users_table_cannot_delete_or_edit": "Kan ikke slette eller redigere en aktiv bruger",
|
||||
"publish_dialog_title_no_topic": "Udgiv notifikation",
|
||||
"publish_dialog_attach_label": "URL til vedhæftede filer",
|
||||
"nav_button_muted": "Notifikationer slået fra",
|
||||
"prefs_notifications_min_priority_description_x_or_higher": "Vis notifikationer hvis prioritet er {{number}} ({{name}}) eller højere",
|
||||
"reservation_delete_dialog_description": "Fjernelse af en reservation opgiver ejerskabet over emnet og giver andre mulighed for at reservere det. Du kan beholde eller slette eksisterende beskeder og vedhæftede filer.",
|
||||
"prefs_reservations_table_everyone_read_write": "Alle kan udgive og abonnere",
|
||||
"account_upgrade_dialog_interval_monthly": "månedlig",
|
||||
"account_basics_phone_numbers_no_phone_numbers_yet": "Ingen telefonnumre endnu",
|
||||
"notifications_no_subscriptions_description": "Klik på linket \"{{linktext}}\" for at oprette eller abonnere på et emne. Derefter kan du sende beskeder via PUT eller POST, og du vil modtage notifikationer her.",
|
||||
"publish_dialog_message_published": "Notifikation udgivet",
|
||||
"publish_dialog_chip_call_label": "Telefon opkald",
|
||||
"account_basics_phone_numbers_dialog_title": "Tilføj telefonnummer",
|
||||
"account_tokens_delete_dialog_description": "Før du sletter et adgangstoken, skal du sikre dig, at ingen programmer eller scripts aktivt bruger det. <strong>Denne handling kan ikke fortrydes</strong>.",
|
||||
"account_upgrade_dialog_billing_contact_website": "For spørgsmål om fakturering, se venligst vores <Link>hjemmeside</Link>.",
|
||||
"account_usage_reservations_none": "Ingen reserverede emner til denne konto",
|
||||
"account_tokens_description": "Brug adgangstokens, når du udgiver og abonnerer via ntfy API, så du ikke behøver at sende dine kontooplysninger. Tjek <Link>dokumentationen</Link> for at få mere at vide.",
|
||||
"prefs_reservations_table": "Reserverede emner tabel",
|
||||
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} daglig e-mail",
|
||||
"prefs_reservations_description": "Her kan du reservere emnenavne til personlig brug. Reservering af et emne giver dig ejerskab over emnet og giver dig mulighed for at definere adgangstilladelser for andre brugere over emnet.",
|
||||
"prefs_users_description_no_sync": "Brugere og adgangskoder er ikke synkroniseret til din konto.",
|
||||
"nav_button_publish_message": "Udgiv notifikation",
|
||||
"prefs_users_table_base_url_header": "Tjeneste-URL",
|
||||
"publish_dialog_attach_reset": "Fjern URL til vedhæftede filer",
|
||||
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} daglig besked",
|
||||
"account_upgrade_dialog_reservations_warning_one": "Det valgte niveau tillader færre reserverede emner end dit nuværende niveau. Før du ændrer dit niveau, <strong>slet venligst mindst én reservation</strong>. Du kan fjerne reservationer i <Link>Indstillinger</Link>.",
|
||||
"error_boundary_unsupported_indexeddb_description": "ntfy-webappen har brug for IndexedDB for at fungere, og din browser understøtter ikke IndexedDB i privat browsing-tilstand.<br/><br/>Selv om dette er uheldigt, giver det heller ikke ret meget mening at bruge ntfy-webappen i privat browsing-tilstand alligevel, fordi alt er gemt i browserens lager. Du kan læse mere om det <githubLink>i dette GitHub issue</githubLink>, eller tale med os på <discordLink>Discord</discordLink> eller <matrixLink>Matrix</matrixLink>.",
|
||||
"publish_dialog_title_placeholder": "Notifikationstitel, f.eks. Advarsel om diskplads",
|
||||
"account_basics_tier_description": "Din kontos niveau",
|
||||
"account_basics_phone_numbers_description": "For notifikationer via telefonopkald",
|
||||
"account_upgrade_dialog_cancel_warning": "Dette vil <strong>annullere dit abonnement</strong> og nedgradere din konto den {{date}}. På den dato <strong>slettes</strong> emnereservationer samt meddelelser, der er gemt på serveren.",
|
||||
"publish_dialog_chip_call_no_verified_numbers_tooltip": "Ingen verificerede telefonnumre",
|
||||
"publish_dialog_call_label": "Telefon opkald",
|
||||
"account_usage_calls_title": "Telefonopkald foretaget",
|
||||
"prefs_notifications_min_priority_description_any": "Viser alle notifikationer, uanset prioritet",
|
||||
"error_boundary_gathering_info": "Indsaml mere info…",
|
||||
"reservation_delete_dialog_action_keep_description": "Beskeder og vedhæftede filer, der er cachelagret på serveren, bliver offentligt synlige for personer med kendskab til emnenavnet.",
|
||||
"account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopieret til udklipsholder",
|
||||
"prefs_reservations_dialog_description": "Reservering af et emne giver dig ejerskab over emnet og giver dig mulighed for at definere adgangstilladelser for andre brugere over emnet.",
|
||||
"publish_dialog_title_topic": "Udgiv til {{topic}}",
|
||||
"account_basics_phone_numbers_dialog_number_placeholder": "f.eks. +4512345678",
|
||||
"account_basics_phone_numbers_dialog_code_placeholder": "f.eks. 123456",
|
||||
"account_basics_username_description": "Hej, der er du ❤",
|
||||
"publish_dialog_base_url_placeholder": "Tjeneste-URL, f.eks. https://example.com",
|
||||
"account_basics_tier_interval_yearly": "årligt",
|
||||
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} årligt. Faktureres månedligt.",
|
||||
"account_basics_phone_numbers_dialog_channel_call": "Opkald",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "overskrider filgrænsen og kvoten på {{fileSizeLimit}}, {{remainingBytes}} tilbage",
|
||||
"account_upgrade_dialog_interval_yearly": "årligt",
|
||||
"account_upgrade_dialog_tier_price_billed_yearly": "{{price}} faktureres årligt. Spar {{save}}.",
|
||||
"account_usage_basis_ip_description": "Brugsstatistikker og begrænsninger for denne konto er baseret på din IP-adresse, så de kan være delt med andre brugere. Ovenstående grænser er omtrentlige baseret på de eksisterende hastigheds grænser.",
|
||||
"account_basics_password_dialog_title": "Skift kodeord",
|
||||
"account_basics_phone_numbers_title": "Telefonnumre",
|
||||
"account_upgrade_dialog_interval_yearly_discount_save": "spar {{discount}}%",
|
||||
"publish_dialog_drop_file_here": "Smid filen her",
|
||||
"prefs_reservations_table_everyone_write_only": "Jeg kan udgive og abonnere, alle kan udgive",
|
||||
"account_tokens_table_cannot_delete_or_edit": "Kan ikke redigere eller slette nuværende sessionstoken",
|
||||
"publish_dialog_attached_file_filename_placeholder": "Vedhæftet filnavn",
|
||||
"subscribe_dialog_subscribe_base_url_label": "Tjeneste-URL",
|
||||
"account_upgrade_dialog_tier_price_per_month": "måned",
|
||||
"message_bar_show_dialog": "Vis udgivelsesdialogen",
|
||||
"account_usage_calls_none": "Der kan ikke foretages telefonopkald med denne konto",
|
||||
"nav_upgrade_banner_description": "Reserver emner, flere beskeder og e-mails og større vedhæftede filer",
|
||||
"publish_dialog_call_reset": "Fjern telefon opkald",
|
||||
"account_basics_phone_numbers_dialog_code_label": "Verifikationskode",
|
||||
"reservation_delete_dialog_action_delete_description": "Cachelagrede beskeder og vedhæftede filer slettes permanent. Denne handling kan ikke fortrydes.",
|
||||
"alert_grant_button": "Tillad nu",
|
||||
"account_usage_attachment_storage_description": "{{filesize}} pr. fil, slettet efter {{expiry}}",
|
||||
"publish_dialog_chip_click_label": "Klik på URL",
|
||||
"account_basics_phone_numbers_dialog_verify_button_call": "Ring til mig",
|
||||
"publish_dialog_call_item": "Ring til tlf. {{number}}",
|
||||
"prefs_users_dialog_base_url_label": "Tjeneste-URL, f.eks. https://ntfy.sh",
|
||||
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
|
||||
"account_delete_dialog_billing_warning": "Hvis du sletter din konto, så annulleres dit abonnement med det samme. Du vil ikke længere have adgang til faktureringspanelet.",
|
||||
"prefs_notifications_min_priority_description_max": "Vis notifikationer, hvis prioritet er 5 (maks.)",
|
||||
"account_upgrade_dialog_reservations_warning_other": "Det valgte niveau tillader færre reserverede emner end dit nuværende niveau. Før du ændrer dit niveau, <strong>slet venligst mindst {{count}} reservationer</strong>. Du kan fjerne reservationer i <Link>Indstillinger</Link>."
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"notifications_click_copy_url_title": "Link-URL in Zwischenablage kopieren",
|
||||
"publish_dialog_priority_low": "Niedrige Priorität",
|
||||
"publish_dialog_message_label": "Nachricht",
|
||||
"action_bar_unsubscribe": "Abmelden",
|
||||
"action_bar_unsubscribe": "Abbestellen",
|
||||
"notifications_copied_to_clipboard": "In Zwischenablage kopiert",
|
||||
"notifications_loading": "Benachrichtigungen werden geladen …",
|
||||
"notifications_attachment_open_title": "Gehe zu {{url}}",
|
||||
|
@ -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 welche 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",
|
||||
|
@ -180,7 +180,7 @@
|
|||
"error_boundary_unsupported_indexeddb_description": "Die ntfy Web-App benötigt eine IndexedDB für eine korrekte Funktion, und Dein Browser unterstützt in privaten Tabs keinen IndexedDB.<br/><br/>Das ist zwar ärgerlich, eine Nutzung von ntfy in einem privaten Tab macht aber auch wenig Sinn da alle Daten im Browser gespeichert werden. Weitere Informationen gibt es <githubLink>in diesem GitHub-Issue</githubLink>, oder im Chat bei <discordLink>Discord</discordLink> oder <matrixLink>Matrix</matrixLink>.",
|
||||
"action_bar_toggle_action_menu": "Aktionsmenü öffnen/schließen",
|
||||
"notifications_new_indicator": "Neue Benachrichtigung",
|
||||
"publish_dialog_email_reset": "Email-Weiterleitung entfernen",
|
||||
"publish_dialog_email_reset": "E-Mail-Weiterleitung entfernen",
|
||||
"action_bar_logo_alt": "ntfy Logo",
|
||||
"nav_button_muted": "Benachrichtigungen stummgeschaltet",
|
||||
"notifications_list_item": "Benachrichtigung",
|
||||
|
@ -217,7 +217,7 @@
|
|||
"signup_form_password": "Kennwort",
|
||||
"signup_form_toggle_password_visibility": "Kennwort-Sichtbarkeit umschalten",
|
||||
"nav_button_account": "Konto",
|
||||
"nav_upgrade_banner_description": "Themen reservieren, mehr Nachrichten & Emails, größere Anhänge",
|
||||
"nav_upgrade_banner_description": "Themen reservieren, mehr Nachrichten & E-Mails und größere Anhänge",
|
||||
"display_name_dialog_title": "Anzeigennamen ändern",
|
||||
"display_name_dialog_placeholder": "Anzeigename",
|
||||
"reserve_dialog_checkbox_label": "Thema reservieren und Zugriffsrechte konfigurieren",
|
||||
|
@ -245,7 +245,7 @@
|
|||
"account_basics_tier_payment_overdue": "Deine Zahlung ist überfällig. Bitte aktualisiere Deine Zahlungsmethode, oder Dein Konto wird herabgestuft.",
|
||||
"account_basics_tier_manage_billing_button": "Zahlung verwalten",
|
||||
"account_usage_messages_title": "Veröffentlichte Nachrichten",
|
||||
"account_usage_emails_title": "Gesendete Emails",
|
||||
"account_usage_emails_title": "Gesendete E-Mails",
|
||||
"account_usage_reservations_title": "Reservierte Themen",
|
||||
"account_usage_reservations_none": "Keine reservierten Themen für dieses Konto",
|
||||
"account_usage_attachment_storage_title": "Speicherplatz für Anhänge",
|
||||
|
@ -266,7 +266,7 @@
|
|||
"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_other": "{{reservations}} reservierte Themen",
|
||||
"account_upgrade_dialog_tier_features_messages_other": "{{messages}} Nachrichten pro Tag",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} Emails pro Tag",
|
||||
"account_upgrade_dialog_tier_features_emails_other": "{{emails}} E-Mails pro Tag",
|
||||
"account_upgrade_dialog_tier_features_attachment_file_size": "{{filesize}} pro Datei",
|
||||
"account_upgrade_dialog_tier_features_attachment_total_size": "{{totalsize}} gesamter Speicherplatz",
|
||||
"account_upgrade_dialog_tier_selected_label": "Ausgewählt",
|
||||
|
|
1
web/public/static/langs/eo.json
Normal file
1
web/public/static/langs/eo.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -60,7 +60,7 @@
|
|||
"publish_dialog_tags_placeholder": "Pilkuilla eroteltu luettelo tunnisteista, esim. varoitus, srv1-varmuuskopio",
|
||||
"account_delete_title": "Poista tili",
|
||||
"publish_dialog_attached_file_remove": "Poista liitetiedosto",
|
||||
"nav_button_connecting": "yhdistää",
|
||||
"nav_button_connecting": "yhdistetään",
|
||||
"account_delete_dialog_label": "Salasana",
|
||||
"subscribe_dialog_login_button_login": "Kirjaudu",
|
||||
"account_upgrade_dialog_tier_features_no_reservations": "Ei varattuja topikkeja",
|
||||
|
@ -89,7 +89,7 @@
|
|||
"action_bar_toggle_mute": "Hiljennä/poista hiljennys",
|
||||
"reservation_delete_dialog_submit_button": "Poista varaus",
|
||||
"account_basics_title": "Tili",
|
||||
"nav_button_documentation": "Käyttäjä oppaat",
|
||||
"nav_button_documentation": "Dokumentointi",
|
||||
"prefs_reservations_limit_reached": "Olet saavuttanut varattujen topikkien rajan.",
|
||||
"account_upgrade_dialog_interval_monthly": "Kuukausittain",
|
||||
"prefs_users_add_button": "Lisää käyttäjä",
|
||||
|
@ -142,7 +142,7 @@
|
|||
"account_basics_tier_upgrade_button": "Päivitä Pro versioon",
|
||||
"prefs_users_description_no_sync": "Käyttäjiä ja salasanoja ei ole synkronoitu tiliisi.",
|
||||
"account_tokens_dialog_title_edit": "Muokkaa käyttöoikeustunnusta",
|
||||
"nav_button_publish_message": "Julkaisutiedot",
|
||||
"nav_button_publish_message": "Julkaise ilmoitus",
|
||||
"prefs_users_table_base_url_header": "Palvelin URL",
|
||||
"notifications_click_copy_url_title": "Kopioi linkin URL-osoite leikepöydälle",
|
||||
"publish_dialog_attach_reset": "Poista liitteen URL-osoite",
|
||||
|
@ -160,7 +160,7 @@
|
|||
"publish_dialog_priority_low": "Matala tärkeys",
|
||||
"publish_dialog_priority_label": "Prioriteetti",
|
||||
"prefs_reservations_delete_button": "Poista topikin oikeudet",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(no tier)",
|
||||
"account_basics_tier_admin_suffix_no_tier": "(e tasoa)",
|
||||
"prefs_notifications_delete_after_one_week_description": "Ilmoitukset poistetaan automaattisesti viikon kuluttua",
|
||||
"error_boundary_unsupported_indexeddb_description": "Ntfy-verkkosovellus tarvitsee IndexedDB:n toimiakseen, eikä selaimesi tue IndexedDB:tä yksityisessä selaustilassa.<br/><br/>Vaikka tämä on valitettavaa, ntfy-verkon käyttäminen ei myöskään ole kovin järkevää yksityisessä selaustilassa, koska kaikki on tallennettu selaimen tallennustilaan. Voit lukea siitä lisää <githubLink>tästä GitHub-numerosta</githubLink> tai puhua meille <discordLink>Discordissa</discordLink> tai <matrixLink>Matrixissa</matrixLink>.",
|
||||
"subscribe_dialog_subscribe_button_cancel": "Peruuta",
|
||||
|
@ -175,7 +175,7 @@
|
|||
"notifications_click_copy_url_button": "Kopioi linkki",
|
||||
"account_basics_tier_admin": "Admin",
|
||||
"subscribe_dialog_subscribe_title": "Tilaa topikki",
|
||||
"nav_topics_title": "Tilatut topikit",
|
||||
"nav_topics_title": "Tilatut aiheet",
|
||||
"prefs_notifications_sound_title": "Ilmoitusääni",
|
||||
"prefs_notifications_min_priority_default_and_higher": "Oletusprioriteetti ja korkeammat",
|
||||
"prefs_reservations_table_access_header": "Oikeudet",
|
||||
|
@ -232,7 +232,7 @@
|
|||
"account_basics_username_description": "Hei, se olet sinä ❤",
|
||||
"prefs_reservations_dialog_topic_label": "Topik",
|
||||
"account_basics_password_dialog_confirm_password_label": "Vahvista salasana",
|
||||
"action_bar_reservation_edit": "Muokkaa varatopikkia",
|
||||
"action_bar_reservation_edit": "Muokkaa varausta",
|
||||
"publish_dialog_base_url_placeholder": "Palvelun URL-osoite, esim. https://example.com",
|
||||
"prefs_users_title": "Hallinnoi käyttäjiä",
|
||||
"account_basics_tier_interval_yearly": "vuosittain",
|
||||
|
@ -247,7 +247,7 @@
|
|||
"publish_dialog_delay_label": "Viive",
|
||||
"error_boundary_button_copy_stack_trace": "Kopioi pinon jälki",
|
||||
"publish_dialog_button_send": "Lähetä",
|
||||
"action_bar_reservation_delete": "Poista varatopikit",
|
||||
"action_bar_reservation_delete": "Poista varaus",
|
||||
"publish_dialog_button_cancel_sending": "Peruuta lähetys",
|
||||
"account_tokens_dialog_title_delete": "Poista käyttöoikeustunnus",
|
||||
"account_usage_of_limit": "limiitistä {{limit}}",
|
||||
|
@ -260,7 +260,7 @@
|
|||
"account_upgrade_dialog_interval_yearly": "Vuosittain",
|
||||
"publish_dialog_tags_label": "Tagit",
|
||||
"signup_form_password": "Salasana",
|
||||
"action_bar_reservation_limit_reached": "Varatopikien raja",
|
||||
"action_bar_reservation_limit_reached": "Raja saavutettu",
|
||||
"account_upgrade_dialog_button_redirect_signup": "Kirjaudu nyt",
|
||||
"publish_dialog_click_placeholder": "URL-osoite, joka avautuu, kun ilmoitusta napsautetaan",
|
||||
"alert_not_supported_title": "Ilmoituksia ei tueta",
|
||||
|
@ -314,14 +314,14 @@
|
|||
"notifications_click_open_button": "Avaa linkki",
|
||||
"account_tokens_table_current_session": "Nykyinen selainistunto",
|
||||
"account_upgrade_dialog_button_pay_now": "Maksa nyt ja tilaa",
|
||||
"nav_upgrade_banner_description": "Varaa aiheita, lisää viestejä ja sähköposteja sekä suurempia liitteitä",
|
||||
"nav_upgrade_banner_description": "Varaa aiheita, lisää viestejä ja sähköposteja, sekä suurempia liitteitä",
|
||||
"publish_dialog_call_reset": "Poista puhelu",
|
||||
"publish_dialog_other_features": "Muut ominaisuudet:",
|
||||
"subscribe_dialog_subscribe_use_another_label": "Käytä toista palvelinta",
|
||||
"reservation_delete_dialog_action_delete_title": "Poista välimuistissa olevat viestit ja liitteet",
|
||||
"signup_error_username_taken": "Käyttäjätunnus {{username}} on jo varattu",
|
||||
"account_basics_phone_numbers_dialog_code_label": "Vahvistuskoodi",
|
||||
"nav_button_subscribe": "Tilaa topik",
|
||||
"nav_button_subscribe": "Tilaa aihe",
|
||||
"publish_dialog_topic_label": "Topikin nimi",
|
||||
"reservation_delete_dialog_action_delete_description": "Välimuistissa olevat viestit ja liitteet poistetaan pysyvästi. Tätä toimintoa ei voi kumota.",
|
||||
"alert_grant_button": "Myönnä nyt",
|
||||
|
@ -341,7 +341,7 @@
|
|||
"prefs_users_dialog_base_url_label": "Palvelin URL, esim. https://ntfy.sh",
|
||||
"account_usage_emails_title": "Sähköpostit lähetetty",
|
||||
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
|
||||
"action_bar_reservation_add": "Varalla oleva topikki",
|
||||
"action_bar_reservation_add": "Varalla oleva aihe",
|
||||
"account_upgrade_dialog_tier_selected_label": "Valittu",
|
||||
"account_upgrade_dialog_button_update_subscription": "Päivitä tilaus",
|
||||
"notifications_attachment_file_video": "videotiedosto",
|
||||
|
@ -363,6 +363,22 @@
|
|||
"notifications_none_for_topic_description": "Jos haluat lähettää ilmoituksia tähän topikkiin, PUT tai POST topikin URL-osoitteeseen.",
|
||||
"notifications_none_for_any_description": "Jos haluat lähettää ilmoituksia topikkiin, PUT tai POST topikin URL-osoitteeseen. Tässä on esimerkki yhden topikin käyttämisestä.",
|
||||
"notifications_no_subscriptions_title": "Näyttää siltä, että sinulla ei ole vielä tilauksia.",
|
||||
"notifications_none_for_topic_title": "Et ole vielä saanut ilmoituksia tästä topikista.",
|
||||
"notifications_actions_http_request_title": "Lähetä HTTP {{method}} to {{url}}"
|
||||
"notifications_none_for_topic_title": "Et ole vielä saanut ilmoituksia tästä aiheesta.",
|
||||
"notifications_actions_http_request_title": "Lähetä HTTP {{method}} to {{url}}",
|
||||
"reserve_dialog_checkbox_label": "Käänteinen aihe ja aseta pääsy",
|
||||
"publish_dialog_progress_uploading": "Lähetetään …",
|
||||
"publish_dialog_title_no_topic": "Julkaise ilmoitus",
|
||||
"notifications_example": "Esimerkki",
|
||||
"notifications_loading": "Ladataan ilmoituksia …",
|
||||
"notifications_no_subscriptions_description": "Klikkaa \"{{linktext}}\" linkkiä luodaksesi tai tilataksesi aihe. Sen jälkeen voit lähettää viestejä PUT tai POST metodeilla ja saat ilmoituksesi täällä.",
|
||||
"display_name_dialog_description": "Aseta vaihtoehtoinen nimi aiheelle, joka on näytetty tilaus-listassa. Tämä auttaa tunnistamaan aiheet helpommin, joilla on hankalat nimet.",
|
||||
"publish_dialog_message_published": "Ilmoitus julkaistu",
|
||||
"notifications_more_details": "Saadaksesi lisää tietoa, katso <websiteLink>nettisivu</websiteLink> tai <docsLink>documentointi</docsLink>.",
|
||||
"publish_dialog_attachment_limits_quota_reached": "ylittää kiintiön, {{remainingBytes}} jäljellä",
|
||||
"publish_dialog_title_topic": "Julkaise aiheeseen {{topic}}",
|
||||
"display_name_dialog_placeholder": "Näyttönimi",
|
||||
"publish_dialog_attachment_limits_file_and_quota_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan ja määrän, {{remainingBytes}} jäljellä",
|
||||
"publish_dialog_attachment_limits_file_reached": "ylittää {{fileSizeLimit}} tiedostokoon rajan",
|
||||
"publish_dialog_progress_uploading_detail": "Lähetetään {{loaded}}/{{total}} ({{percent}}%) …",
|
||||
"display_name_dialog_title": "Vaihda näyttönimi"
|
||||
}
|
||||
|
|
|
@ -101,5 +101,27 @@
|
|||
"notifications_click_open_button": "Deschide link",
|
||||
"publish_dialog_emoji_picker_show": "Alege un emoji",
|
||||
"notifications_loading": "Încărcare notificări…",
|
||||
"publish_dialog_priority_low": "Prioritate joasă"
|
||||
"publish_dialog_priority_low": "Prioritate joasă",
|
||||
"signup_form_username": "Nume de utilizator",
|
||||
"signup_form_button_submit": "Înscrie-te",
|
||||
"common_copy_to_clipboard": "Copiază în clipboard",
|
||||
"signup_form_toggle_password_visibility": "Schimbă vizibilitatea parolei",
|
||||
"signup_title": "Crează un cont ntfy",
|
||||
"signup_already_have_account": "Deja ai un cont? Autentifică-te!",
|
||||
"login_disabled": "Autentificarea este dezactivată",
|
||||
"signup_error_creation_limit_reached": "S-a atins limita de conturi",
|
||||
"action_bar_toggle_action_menu": "Deschide/Închide meniul de acțiuni",
|
||||
"action_bar_sign_up": "Înscriere",
|
||||
"message_bar_publish": "Publică mesajul",
|
||||
"login_link_signup": "Înscrie-te",
|
||||
"action_bar_sign_in": "Autentificare",
|
||||
"action_bar_reservation_edit": "Schimbă rezervarea",
|
||||
"action_bar_reservation_delete": "Șterge rezervarea",
|
||||
"login_form_button_submit": "Autentifică-te",
|
||||
"signup_disabled": "Înscrierea este dezactivată",
|
||||
"action_bar_profile_logout": "Ieșire",
|
||||
"message_bar_show_dialog": "Arată dialogul de publicare",
|
||||
"signup_error_username_taken": "Numele de utilizator {{username}} este deja folosit",
|
||||
"login_title": "Autentifică-te în contul ntfy",
|
||||
"action_bar_reservation_add": "Rezervă topicul"
|
||||
}
|
||||
|
|
27
web/public/static/langs/uz.json
Normal file
27
web/public/static/langs/uz.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"signup_title": "ntfy hisobini yaratish",
|
||||
"signup_form_password": "Parol",
|
||||
"signup_form_confirm_password": "Parolni tasdiqlang",
|
||||
"signup_error_username_taken": "Foydalanuvchi nomi {{username}} allaqachon foydalanilmoqda",
|
||||
"signup_error_creation_limit_reached": "Boshqa hisob raqam ocha olmaysiz",
|
||||
"login_title": "Ntfy hisobingizga kiring",
|
||||
"login_form_button_submit": "Kirish",
|
||||
"login_link_signup": "Ro'yxatdan o'tish",
|
||||
"login_disabled": "Kirish o'chirilgan",
|
||||
"action_bar_show_menu": "Menyuni ko'rsatish",
|
||||
"action_bar_logo_alt": "ntfy logotipi",
|
||||
"action_bar_settings": "Sozlamalar",
|
||||
"action_bar_change_display_name": "Ko'rsatilgan nomni o'zgartiring",
|
||||
"action_bar_reservation_add": "Zaxira mavzusi",
|
||||
"common_cancel": "Bekor qilish",
|
||||
"common_save": "Saqlash",
|
||||
"common_add": "Qo‘shish",
|
||||
"common_back": "Orqaga",
|
||||
"common_copy_to_clipboard": "Xotiraga nusxalash",
|
||||
"signup_form_username": "Foydalanuvchi nomi",
|
||||
"signup_form_button_submit": "Ro‘yxatdan o‘tish",
|
||||
"signup_form_toggle_password_visibility": "Parol ko‘rinishini o‘zgartirish",
|
||||
"signup_already_have_account": "Hisobingiz bormi? Tizimga kiring!",
|
||||
"signup_disabled": "Ro‘yxatdan o‘tish o‘chirilgan",
|
||||
"action_bar_account": "Hisob"
|
||||
}
|
Loading…
Reference in a new issue