diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3bf2a126..00000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -*/node_modules -Dockerfile* diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index de22292a..0076c0fa 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,7 +11,7 @@ jobs: name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.20.x' + go-version: '1.19.x' - name: Install node uses: actions/setup-node@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b61e3361..f709332a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,7 +14,7 @@ jobs: name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.20.x' + go-version: '1.19.x' - name: Install node uses: actions/setup-node@v3 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f76862a9..7473567b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ jobs: name: Install Go uses: actions/setup-go@v4 with: - go-version: '1.20.x' + go-version: '1.19.x' - name: Install node uses: actions/setup-node@v3 diff --git a/.gitignore b/.gitignore index b60c9b23..b0c2d330 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ dist/ -dev-dist/ build/ .idea/ .vscode/ @@ -13,4 +12,3 @@ secrets/ node_modules/ .DS_Store __pycache__ -web/dev-dist/ \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 3c3aa490..131a302a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -71,7 +71,7 @@ builds: nfpms: - package_name: ntfy - homepage: https://git.zio.sh/astra/ntfy/v2 + homepage: https://heckel.io/ntfy maintainer: Philipp C. Heckel description: Simple pub-sub notification service license: Apache 2.0 @@ -119,6 +119,8 @@ archives: - server/ntfy.service - client/client.yml - client/ntfy-client.service + replacements: + amd64: x86_64 - id: ntfy_windows builds: @@ -129,6 +131,8 @@ archives: - LICENSE - README.md - client/client.yml + replacements: + amd64: x86_64 - id: ntfy_darwin builds: @@ -138,6 +142,8 @@ archives: - LICENSE - README.md - client/client.yml + replacements: + darwin: macOS universal_binaries: - id: ntfy_darwin_all @@ -164,14 +170,14 @@ dockers: - image_templates: - &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8" use: buildx - dockerfile: Dockerfile-arm + dockerfile: Dockerfile goarch: arm64 build_flag_templates: - "--platform=linux/arm64/v8" - image_templates: - &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7" use: buildx - dockerfile: Dockerfile-arm + dockerfile: Dockerfile goarch: arm goarm: 7 build_flag_templates: @@ -179,7 +185,7 @@ dockers: - image_templates: - &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6" use: buildx - dockerfile: Dockerfile-arm + dockerfile: Dockerfile goarch: arm goarm: 6 build_flag_templates: diff --git a/Dockerfile b/Dockerfile index 45dad05d..7c2052ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,6 @@ 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 COPY ntfy /usr/bin EXPOSE 80/tcp diff --git a/Dockerfile-arm b/Dockerfile-arm deleted file mode 100644 index 755092fd..00000000 --- a/Dockerfile-arm +++ /dev/null @@ -1,18 +0,0 @@ -FROM alpine - -LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" -LABEL org.opencontainers.image.url="https://ntfy.sh/" -LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" -LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy" -LABEL org.opencontainers.image.vendor="Philipp C. Heckel" -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" - -# Alpine does not support adding "tzdata" on ARM anymore, see -# https://github.com/binwiederhier/ntfy/issues/894 - -COPY ntfy /usr/bin - -EXPOSE 80/tcp -ENTRYPOINT ["ntfy"] diff --git a/Dockerfile-build b/Dockerfile-build deleted file mode 100644 index 6e96c7d4..00000000 --- a/Dockerfile-build +++ /dev/null @@ -1,57 +0,0 @@ -FROM golang:1.20-bullseye as builder - -ARG VERSION=dev -ARG COMMIT=unknown -ARG NODE_MAJOR=18 - -RUN apt-get update && apt-get install -y \ - build-essential ca-certificates curl gnupg \ - && mkdir -p /etc/apt/keyrings \ - && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ - && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \ - && apt-get update \ - && apt-get install -y \ - python3-pip nodejs \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -ADD Makefile . - -# docs -ADD ./requirements.txt . -RUN make docs-deps -ADD ./mkdocs.yml . -ADD ./docs ./docs -RUN make docs-build - -# web -ADD ./web/package.json ./web/package-lock.json ./web/ -RUN make web-deps -ADD ./web ./web -RUN make web-build - -# cli & server -ADD go.mod go.sum main.go ./ -ADD ./client ./client -ADD ./cmd ./cmd -ADD ./log ./log -ADD ./server ./server -ADD ./user ./user -ADD ./util ./util -RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server - -FROM alpine - -LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com" -LABEL org.opencontainers.image.url="https://ntfy.sh/" -LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/" -LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy" -LABEL org.opencontainers.image.vendor="Philipp C. Heckel" -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" - -COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy - -EXPOSE 80/tcp -ENTRYPOINT ["ntfy"] diff --git a/Makefile b/Makefile index 88c22033..73988446 100644 --- a/Makefile +++ b/Makefile @@ -31,16 +31,12 @@ help: @echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)" @echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)" @echo - @echo "Build dev Docker:" - @echo " make docker-dev - Build client & server for current architecture using Docker only" - @echo @echo "Build web app:" @echo " make web - Build the web app" @echo " make web-deps - Install web app dependencies (npm install the universe)" @echo " make web-build - Actually build the web app" - @echo " make web-lint - Run eslint on the web app" - @echo " make web-fmt - Run prettier on the web app" - @echo " make web-fmt-check - Run prettier on the web app, but don't change anything" + @echo " make web-format - Run prettier on the web app + @echo " make web-format-check - Run prettier on the web app, but don't change anything @echo @echo "Build documentation:" @echo " make docs - Build the documentation" @@ -86,32 +82,34 @@ build: web docs cli update: web-deps-update cli-deps-update docs-deps-update docker pull alpine -docker-dev: - docker build \ - --file ./Dockerfile-build \ - --tag binwiederhier/ntfy:$(VERSION) \ - --tag binwiederhier/ntfy:dev \ - --build-arg VERSION=$(VERSION) \ - --build-arg COMMIT=$(COMMIT) \ - ./ - # Ubuntu-specific build-deps-ubuntu: - sudo apt-get update - sudo apt-get install -y \ + sudo apt update + sudo apt install -y \ curl \ gcc-aarch64-linux-gnu \ gcc-arm-linux-gnueabi \ jq - which pip3 || sudo apt-get install -y python3-pip + which pip3 || sudo apt install -y python3-pip # Documentation docs: docs-deps docs-build docs-build: .PHONY - mkdocs build + @if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \ + if which python3.8; then \ + echo "python3.8 $(shell which mkdocs) build"; \ + python3.8 $(shell which mkdocs) build; \ + else \ + echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \ + exit 1; \ + fi; \ + else \ + echo "mkdocs build"; \ + mkdocs build; \ + fi docs-deps: .PHONY pip3 install -r requirements.txt @@ -140,10 +138,10 @@ web-deps: web-deps-update: cd web && npm update -web-fmt: +web-format: cd web && npm run format -web-fmt-check: +web-format-check: cd web && npm run format:check web-lint: @@ -237,7 +235,7 @@ cli-build-results: # Test/check targets -check: test web-fmt-check fmt-check vet web-lint lint staticcheck +check: test web-format-check fmt-check vet web-lint lint staticcheck test: .PHONY go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)') @@ -264,7 +262,7 @@ coverage-upload: # Lint/formatting targets -fmt: web-fmt +fmt: gofmt -s -w . fmt-check: diff --git a/README.md b/README.md index b3d0c55e..cebf55be 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,180 @@ +![ntfy](web/public/static/images/ntfy.png) + # ntfy.sh | Send push notifications to your phone or desktop via PUT/POST +[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest) +[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy) +[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions) +[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy) +[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy) +[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w) +[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org) +[![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org) +[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/ntfy?color=%23317f6f&label=-%20r%2Fntfy&style=social)](https://www.reddit.com/r/ntfy/) +[![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/) +[![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy) **ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer, **without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do so since ntfy is open source. +You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open source Android app](https://github.com/binwiederhier/ntfy-android) +available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/), +as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). -### This is a fork of [github.com/binwiederhier/ntfy](https://github.com/binwiederhier/ntfy) \ No newline at end of file +

+ + + + + +

+ +## [ntfy Pro](https://ntfy.sh/app) 💸 🎉 +I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $3.33/month** (if you use promo code `MYTOPIC`, limited time only). You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy). I would be very humbled by your sponsorship. ❤️ + +## **[Documentation](https://ntfy.sh/docs/)** + +[Getting started](https://ntfy.sh/docs/) | +[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) | +[API](https://ntfy.sh/docs/publish/) | +[Install / Self-hosting](https://ntfy.sh/docs/install/) | +[Building](https://ntfy.sh/docs/develop/) + +## Chat / forum +There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever +works best for you: + +* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community +* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord +* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_) +* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs +* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_) + +## Announcements / beta testers +For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) +topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, +join Discord/Matrix (I'll eventually make a testing channel in Google Play). + +## Contributing +I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out +on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) +for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in +[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/). + + +Translation status + + +## Sponsors +I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), +and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer +account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free, +and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project: + + + +## Code of Conduct +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.** + +_Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._ + +## License +Made with ❤️ by [Philipp C. Heckel](https://heckel.io). +The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2). + +Third party libraries and resources: +* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI +* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds +* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds +* [Roboto Font](https://fonts.google.com/specimen/Roboto) (Apache 2.0) is used as a font in everything web +* [React](https://reactjs.org/) (MIT) is used for the web app +* [Material UI components](https://mui.com/) (MIT) are used in the web app +* [MUI dashboard template](https://github.com/mui/material-ui/tree/master/docs/data/material/getting-started/templates/dashboard) (MIT) was used as a basis for the web app +* [Dexie.js](https://github.com/dexie/Dexie.js) (Apache 2.0) is used for web app persistence in IndexedDB +* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases +* [go-smtp](https://github.com/emersion/go-smtp) (MIT) is used to receive e-mails +* [stretchr/testify](https://github.com/stretchr/testify) (MIT) is used for unit and integration tests +* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache +* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages +* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file) +* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox) as a lightbox on the landing page +* [HTTP middleware for gzip compression](https://gist.github.com/CJEnright/bc2d8b8dc0c1389a9feeddb110f822d7) (MIT) is used for serving static files +* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used) +* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) +* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs) diff --git a/client/client.go b/client/client.go index 191df260..b744fa11 100644 --- a/client/client.go +++ b/client/client.go @@ -7,29 +7,27 @@ import ( "encoding/json" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "io" "net/http" - "regexp" "strings" "sync" "time" ) +// Event type constants const ( - // MessageEvent identifies a message event - MessageEvent = "message" + MessageEvent = "message" + KeepaliveEvent = "keepalive" + OpenEvent = "open" + PollRequestEvent = "poll_request" ) const ( maxResponseBytes = 4096 ) -var ( - topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go -) - // Client is the ntfy client that can be used to publish and subscribe to ntfy topics type Client struct { Messages chan *Message @@ -98,14 +96,8 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess // To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache, // WithNoFirebase, and the generic WithHeader. func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) { - topicURL, err := c.expandTopicURL(topic) - if err != nil { - return nil, err - } - req, err := http.NewRequest("POST", topicURL, body) - if err != nil { - return nil, err - } + topicURL := c.expandTopicURL(topic) + req, _ := http.NewRequest("POST", topicURL, body) for _, option := range options { if err := option(req); err != nil { return nil, err @@ -141,14 +133,11 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO // By default, all messages will be returned, but you can change this behavior using a SubscribeOption. // See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam. func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) { - topicURL, err := c.expandTopicURL(topic) - if err != nil { - return nil, err - } ctx := context.Background() messages := make([]*Message, 0) msgChan := make(chan *Message) errChan := make(chan error) + topicURL := c.expandTopicURL(topic) log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL)) options = append(options, WithPoll()) go func() { @@ -177,18 +166,15 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err // Example: // // c := client.New(client.NewConfig()) -// subscriptionID, _ := c.Subscribe("mytopic") +// subscriptionID := c.Subscribe("mytopic") // for m := range c.Messages { // fmt.Printf("New message: %s", m.Message) // } -func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) { - topicURL, err := c.expandTopicURL(topic) - if err != nil { - return "", err - } +func (c *Client) Subscribe(topic string, options ...SubscribeOption) string { c.mu.Lock() defer c.mu.Unlock() subscriptionID := util.RandomString(10) + topicURL := c.expandTopicURL(topic) log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL)) ctx, cancel := context.WithCancel(context.Background()) c.subscriptions[subscriptionID] = &subscription{ @@ -197,7 +183,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, er cancel: cancel, } go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...) - return subscriptionID, nil + return subscriptionID } // Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique @@ -213,16 +199,31 @@ func (c *Client) Unsubscribe(subscriptionID string) { sub.cancel() } -func (c *Client) expandTopicURL(topic string) (string, error) { +// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe. +// If there are multiple subscriptions matching the topic, all of them are unsubscribed from. +// +// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https:// +// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the +// config (e.g. mytopic -> https://ntfy.sh/mytopic). +func (c *Client) UnsubscribeAll(topic string) { + c.mu.Lock() + defer c.mu.Unlock() + topicURL := c.expandTopicURL(topic) + for _, sub := range c.subscriptions { + if sub.topicURL == topicURL { + delete(c.subscriptions, sub.ID) + sub.cancel() + } + } +} + +func (c *Client) expandTopicURL(topic string) string { if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") { - return topic, nil + return topic } else if strings.Contains(topic, "/") { - return fmt.Sprintf("https://%s", topic), nil + return fmt.Sprintf("https://%s", topic) } - if !topicRegex.MatchString(topic) { - return "", fmt.Errorf("invalid topic name: %s", topic) - } - return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil + return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic) } func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) { diff --git a/client/client.yml b/client/client.yml index ebf4c281..1b81b80d 100644 --- a/client/client.yml +++ b/client/client.yml @@ -7,10 +7,7 @@ # Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided. # You can set a default token to use or a default user:password combination, but not both. For an empty password, -# use empty double-quotes (""). -# -# To override the default user:password combination or default token for a particular subscription (e.g., to send -# no Authorization header), set the user:pass/token for the subscription to empty double-quotes (""). +# use empty double-quotes ("") # default-token: diff --git a/client/client_test.go b/client/client_test.go index 7ab39db6..a71ea5cb 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,10 +2,10 @@ package client_test import ( "fmt" - "git.zio.sh/astra/ntfy/v2/client" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/test" "github.com/stretchr/testify/require" + "heckel.io/ntfy/client" + "heckel.io/ntfy/log" + "heckel.io/ntfy/test" "os" "testing" "time" @@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) { defer test.StopServer(t, s, port) c := client.New(newTestConfig(port)) - subscriptionID, _ := c.Subscribe("mytopic") + subscriptionID := c.Subscribe("mytopic") time.Sleep(time.Second) msg, err := c.Publish("mytopic", "some message") diff --git a/client/config.go b/client/config.go index bc46ab89..d4337d47 100644 --- a/client/config.go +++ b/client/config.go @@ -23,9 +23,9 @@ type Config struct { // Subscribe is the struct for a Subscription within Config type Subscribe struct { Topic string `yaml:"topic"` - User *string `yaml:"user"` + User string `yaml:"user"` Password *string `yaml:"password"` - Token *string `yaml:"token"` + Token string `yaml:"token"` Command string `yaml:"command"` If map[string]string `yaml:"if"` } diff --git a/client/config_test.go b/client/config_test.go index f4c86bfb..f22e6b20 100644 --- a/client/config_test.go +++ b/client/config_test.go @@ -1,8 +1,8 @@ package client_test import ( - "git.zio.sh/astra/ntfy/v2/client" "github.com/stretchr/testify/require" + "heckel.io/ntfy/client" "os" "path/filepath" "testing" @@ -37,7 +37,7 @@ subscribe: require.Equal(t, 4, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", *conf.Subscribe[0].User) + require.Equal(t, "phil", conf.Subscribe[0].User) require.Equal(t, "mypass", *conf.Subscribe[0].Password) require.Equal(t, "echo-this", conf.Subscribe[1].Topic) require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command) @@ -67,7 +67,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", *conf.Subscribe[0].User) + require.Equal(t, "phil", conf.Subscribe[0].User) require.Equal(t, "", *conf.Subscribe[0].Password) } @@ -91,7 +91,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", *conf.Subscribe[0].User) + require.Equal(t, "phil", conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) } @@ -113,7 +113,7 @@ subscribe: require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) - require.Equal(t, "phil", *conf.Subscribe[0].User) + require.Equal(t, "phil", conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) } @@ -134,7 +134,7 @@ subscribe: require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken) require.Equal(t, 1, len(conf.Subscribe)) require.Equal(t, "mytopic", conf.Subscribe[0].Topic) - require.Nil(t, conf.Subscribe[0].User) + require.Equal(t, "", conf.Subscribe[0].User) require.Nil(t, conf.Subscribe[0].Password) - require.Nil(t, conf.Subscribe[0].Token) + require.Equal(t, "", conf.Subscribe[0].Token) } diff --git a/client/options.go b/client/options.go index 1bf48faf..dbca8c0e 100644 --- a/client/options.go +++ b/client/options.go @@ -2,7 +2,7 @@ package client import ( "fmt" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" "net/http" "strings" "time" @@ -72,11 +72,6 @@ func WithAttach(attach string) PublishOption { return WithHeader("X-Attach", attach) } -// WithMarkdown instructs the server to interpret the message body as Markdown -func WithMarkdown() PublishOption { - return WithHeader("X-Markdown", "yes") -} - // WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment func WithFilename(filename string) PublishOption { return WithHeader("X-Filename", filename) @@ -97,11 +92,6 @@ func WithBearerAuth(token string) PublishOption { return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token)) } -// WithEmptyAuth clears the Authorization header -func WithEmptyAuth() PublishOption { - return RemoveHeader("Authorization") -} - // WithNoCache instructs the server not to cache the message server-side func WithNoCache() PublishOption { return WithHeader("X-Cache", "no") @@ -192,13 +182,3 @@ func WithQueryParam(param, value string) RequestOption { return nil } } - -// RemoveHeader is a generic option to remove a header from a request -func RemoveHeader(header string) RequestOption { - return func(r *http.Request) error { - if header != "" { - delete(r.Header, header) - } - return nil - } -} diff --git a/cmd/access.go b/cmd/access.go index 0dc4719e..87f01d11 100644 --- a/cmd/access.go +++ b/cmd/access.go @@ -5,9 +5,9 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" ) func init() { diff --git a/cmd/access_test.go b/cmd/access_test.go index d872021a..359beb92 100644 --- a/cmd/access_test.go +++ b/cmd/access_test.go @@ -2,10 +2,10 @@ package cmd import ( "fmt" - "git.zio.sh/astra/ntfy/v2/server" - "git.zio.sh/astra/ntfy/v2/test" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" + "heckel.io/ntfy/test" "testing" ) diff --git a/cmd/app.go b/cmd/app.go index 27e876b6..edef5b47 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -3,9 +3,9 @@ package cmd import ( "fmt" - "git.zio.sh/astra/ntfy/v2/log" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/log" "os" "regexp" ) diff --git a/cmd/app_test.go b/cmd/app_test.go index c5232050..ec27a67d 100644 --- a/cmd/app_test.go +++ b/cmd/app_test.go @@ -3,9 +3,9 @@ package cmd import ( "bytes" "encoding/json" - "git.zio.sh/astra/ntfy/v2/client" - "git.zio.sh/astra/ntfy/v2/log" "github.com/urfave/cli/v2" + "heckel.io/ntfy/client" + "heckel.io/ntfy/log" "os" "strings" "testing" diff --git a/cmd/config_loader.go b/cmd/config_loader.go index eab742e2..9f0a5769 100644 --- a/cmd/config_loader.go +++ b/cmd/config_loader.go @@ -2,10 +2,10 @@ package cmd import ( "fmt" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" "gopkg.in/yaml.v2" + "heckel.io/ntfy/util" "os" ) diff --git a/cmd/publish.go b/cmd/publish.go index 050184ca..0179f9fa 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -3,10 +3,10 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/client" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" + "heckel.io/ntfy/client" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "io" "os" "os/exec" @@ -31,7 +31,6 @@ var flagsPublish = append( &cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"}, &cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"}, - &cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"}, &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"}, @@ -96,7 +95,6 @@ func execPublish(c *cli.Context) error { icon := c.String("icon") actions := c.String("actions") attach := c.String("attach") - markdown := c.Bool("markdown") filename := c.String("filename") file := c.String("file") email := c.String("email") @@ -142,9 +140,6 @@ func execPublish(c *cli.Context) error { if attach != "" { options = append(options, client.WithAttach(attach)) } - if markdown { - options = append(options, client.WithMarkdown()) - } if filename != "" { options = append(options, client.WithFilename(filename)) } diff --git a/cmd/publish_test.go b/cmd/publish_test.go index fb4bbc70..a254f47d 100644 --- a/cmd/publish_test.go +++ b/cmd/publish_test.go @@ -2,9 +2,9 @@ package cmd import ( "fmt" - "git.zio.sh/astra/ntfy/v2/test" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/test" + "heckel.io/ntfy/util" "net/http" "net/http/httptest" "os" diff --git a/cmd/serve.go b/cmd/serve.go index 1177af0c..5d5381bf 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -5,8 +5,8 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" "github.com/stripe/stripe-go/v74" + "heckel.io/ntfy/user" "io/fs" "math" "net" @@ -17,12 +17,12 @@ import ( "syscall" "time" - "git.zio.sh/astra/ntfy/v2/log" + "heckel.io/ntfy/log" - "git.zio.sh/astra/ntfy/v2/server" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/server" + "heckel.io/ntfy/util" ) func init() { @@ -94,11 +94,6 @@ var flagsServe = append( altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), - altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}), ) var cmdServe = &cli.Command{ @@ -134,11 +129,6 @@ func execServe(c *cli.Context) error { keyFile := c.String("key-file") certFile := c.String("cert-file") firebaseKeyFile := c.String("firebase-key-file") - webPushPrivateKey := c.String("web-push-private-key") - webPushPublicKey := c.String("web-push-public-key") - webPushFile := c.String("web-push-file") - webPushEmailAddress := c.String("web-push-email-address") - webPushStartupQueries := c.String("web-push-startup-queries") cacheFile := c.String("cache-file") cacheDuration := c.Duration("cache-duration") cacheStartupQueries := c.String("cache-startup-queries") @@ -193,8 +183,6 @@ func execServe(c *cli.Context) error { // Check values if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { return errors.New("if set, FCM key file must exist") - } else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") { - return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys") } else if keepaliveInterval < 5*time.Second { return errors.New("keepalive interval cannot be lower than five seconds") } else if managerInterval < 5*time.Second { @@ -359,11 +347,6 @@ func execServe(c *cli.Context) error { conf.MetricsListenHTTP = metricsListenHTTP conf.ProfileListenHTTP = profileListenHTTP conf.Version = c.App.Version - conf.WebPushPrivateKey = webPushPrivateKey - conf.WebPushPublicKey = webPushPublicKey - conf.WebPushFile = webPushFile - conf.WebPushEmailAddress = webPushEmailAddress - conf.WebPushStartupQueries = webPushStartupQueries // Set up hot-reloading of config go sigHandlerConfigReload(config) diff --git a/cmd/serve_test.go b/cmd/serve_test.go index 2fef0643..774166c3 100644 --- a/cmd/serve_test.go +++ b/cmd/serve_test.go @@ -9,12 +9,12 @@ import ( "testing" "time" - "git.zio.sh/astra/ntfy/v2/client" - "git.zio.sh/astra/ntfy/v2/test" - "git.zio.sh/astra/ntfy/v2/util" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "heckel.io/ntfy/client" + "heckel.io/ntfy/test" + "heckel.io/ntfy/util" ) func init() { diff --git a/cmd/subscribe.go b/cmd/subscribe.go index c9ed75e9..2691e6a1 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -3,10 +3,10 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/client" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" + "heckel.io/ntfy/client" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "os" "os/exec" "os/user" @@ -72,7 +72,7 @@ ntfy subscribe TOPIC COMMAND $NTFY_TITLE $title, $t Message title $NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max) $NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list) - $NTFY_RAW $raw Raw JSON message + $NTFY_RAW $raw Raw JSON message Examples: ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages @@ -194,10 +194,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, topicOptions = append(topicOptions, auth) } - subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...) - if err != nil { - return err - } + subscriptionID := cl.Subscribe(s.Topic, topicOptions...) if s.Command != "" { cmds[subscriptionID] = s.Command } else if conf.DefaultCommand != "" { @@ -207,10 +204,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, } } if topic != "" { - subscriptionID, err := cl.Subscribe(topic, options...) - if err != nil { - return err - } + subscriptionID := cl.Subscribe(topic, options...) cmds[subscriptionID] = command } for m := range cl.Messages { @@ -225,17 +219,12 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, } func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption { - // if an explicit empty token or empty user:pass is given, exit without auth - if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") { - return client.WithEmptyAuth() - } - // check for subscription token then subscription user:pass - if s.Token != nil && *s.Token != "" { - return client.WithBearerAuth(*s.Token) + if s.Token != "" { + return client.WithBearerAuth(s.Token) } - if s.User != nil && *s.User != "" && s.Password != nil { - return client.WithBasicAuth(*s.User, *s.Password) + if s.User != "" && s.Password != nil { + return client.WithBasicAuth(s.User, *s.Password) } // if no subscription token nor subscription user:pass, check for default token then default user:pass diff --git a/cmd/subscribe_test.go b/cmd/subscribe_test.go index 08dbbf5d..0b3a0a47 100644 --- a/cmd/subscribe_test.go +++ b/cmd/subscribe_test.go @@ -330,7 +330,7 @@ default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 app, _, stdout, _ := newTestApp() - require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"})) + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"})) require.Equal(t, message, strings.TrimSpace(stdout.String())) } @@ -355,63 +355,7 @@ default-password: mypass app, _, stdout, _ := newTestApp() - require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"})) - - require.Equal(t, message, strings.TrimSpace(stdout.String())) -} - -func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) { - message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "/mytopic/json", r.URL.Path) - require.Equal(t, "", r.Header.Get("Authorization")) - - w.WriteHeader(http.StatusOK) - w.Write([]byte(message)) - })) - defer server.Close() - - filename := filepath.Join(t.TempDir(), "client.yml") - require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` -default-host: %s -default-user: philipp -default-password: mypass -subscribe: - - topic: mytopic - user: "" - password: "" -`, server.URL)), 0600)) - - app, _, stdout, _ := newTestApp() - - require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename})) - - require.Equal(t, message, strings.TrimSpace(stdout.String())) -} - -func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) { - message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}` - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, "/mytopic/json", r.URL.Path) - require.Equal(t, "", r.Header.Get("Authorization")) - - w.WriteHeader(http.StatusOK) - w.Write([]byte(message)) - })) - defer server.Close() - - filename := filepath.Join(t.TempDir(), "client.yml") - require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(` -default-host: %s -default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 -subscribe: - - topic: mytopic - token: "" -`, server.URL)), 0600)) - - app, _, stdout, _ := newTestApp() - - require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename})) + require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--config=" + filename, "mytopic"})) require.Equal(t, message, strings.TrimSpace(stdout.String())) } diff --git a/cmd/tier.go b/cmd/tier.go index 76e273a1..f1c8ddcb 100644 --- a/cmd/tier.go +++ b/cmd/tier.go @@ -5,9 +5,9 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" ) func init() { diff --git a/cmd/tier_test.go b/cmd/tier_test.go index b896d15f..1774aa27 100644 --- a/cmd/tier_test.go +++ b/cmd/tier_test.go @@ -1,10 +1,10 @@ package cmd import ( - "git.zio.sh/astra/ntfy/v2/server" - "git.zio.sh/astra/ntfy/v2/test" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" + "heckel.io/ntfy/test" "testing" ) diff --git a/cmd/token.go b/cmd/token.go index 4fe7e541..ab9f4447 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -5,9 +5,9 @@ package cmd import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "net/netip" "time" ) diff --git a/cmd/token_test.go b/cmd/token_test.go index b5f0c0b4..40d7be7b 100644 --- a/cmd/token_test.go +++ b/cmd/token_test.go @@ -2,10 +2,10 @@ package cmd import ( "fmt" - "git.zio.sh/astra/ntfy/v2/server" - "git.zio.sh/astra/ntfy/v2/test" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" + "heckel.io/ntfy/test" "regexp" "testing" ) diff --git a/cmd/user.go b/cmd/user.go index 21fe21af..a96c7089 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -6,13 +6,13 @@ import ( "crypto/subtle" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" + "heckel.io/ntfy/user" "os" "strings" - "git.zio.sh/astra/ntfy/v2/util" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" + "heckel.io/ntfy/util" ) const ( diff --git a/cmd/user_test.go b/cmd/user_test.go index 361a4288..1149285f 100644 --- a/cmd/user_test.go +++ b/cmd/user_test.go @@ -1,11 +1,11 @@ package cmd import ( - "git.zio.sh/astra/ntfy/v2/server" - "git.zio.sh/astra/ntfy/v2/test" - "git.zio.sh/astra/ntfy/v2/user" "github.com/stretchr/testify/require" "github.com/urfave/cli/v2" + "heckel.io/ntfy/server" + "heckel.io/ntfy/test" + "heckel.io/ntfy/user" "os" "path/filepath" "testing" diff --git a/cmd/webpush.go b/cmd/webpush.go deleted file mode 100644 index ec66f083..00000000 --- a/cmd/webpush.go +++ /dev/null @@ -1,48 +0,0 @@ -//go:build !noserver - -package cmd - -import ( - "fmt" - - "github.com/SherClockHolmes/webpush-go" - "github.com/urfave/cli/v2" -) - -func init() { - commands = append(commands, cmdWebPush) -} - -var cmdWebPush = &cli.Command{ - Name: "webpush", - Usage: "Generate keys, in the future manage web push subscriptions", - UsageText: "ntfy webpush [keys]", - Category: categoryServer, - - Subcommands: []*cli.Command{ - { - Action: generateWebPushKeys, - Name: "keys", - Usage: "Generate VAPID keys to enable browser background push notifications", - UsageText: "ntfy webpush keys", - Category: categoryServer, - }, - }, -} - -func generateWebPushKeys(c *cli.Context) error { - privateKey, publicKey, err := webpush.GenerateVAPIDKeys() - if err != nil { - return err - } - _, err = fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file: - -web-push-public-key: %s -web-push-private-key: %s -web-push-file: /var/cache/ntfy/webpush.db # or similar -web-push-email-address: - -See https://ntfy.sh/docs/config/#web-push for details. -`, publicKey, privateKey) - return err -} diff --git a/cmd/webpush_test.go b/cmd/webpush_test.go deleted file mode 100644 index e2565214..00000000 --- a/cmd/webpush_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package cmd - -import ( - "testing" - - "git.zio.sh/astra/ntfy/v2/server" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -func TestCLI_WebPush_GenerateKeys(t *testing.T) { - app, _, _, stderr := newTestApp() - require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys")) - require.Contains(t, stderr.String(), "Web Push keys generated.") -} - -func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error { - webPushArgs := []string{ - "ntfy", - "--log-level=ERROR", - "webpush", - } - return app.Run(append(webPushArgs, args...)) -} diff --git a/docs/config.md b/docs/config.md index 2662a537..df1f2cd6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -44,14 +44,6 @@ Here are a few working sample configs: attachment-cache-dir: "/var/cache/ntfy/attachments" ``` -=== "server.yml (behind proxy, with cache + attachments)" - ``` yaml - base-url: "http://ntfy.example.com" - listen-http: ":2586" - cache-file: "/var/cache/ntfy/cache.db" - attachment-cache-dir: "/var/cache/ntfy/attachments" - ``` - === "server.yml (ntfy.sh config)" ``` yaml # All the things: Behind a proxy, Firebase, cache, attachments, @@ -466,31 +458,6 @@ $ dig A mx1.ntfy.sh +short 3.139.215.220 ``` -### Local-only email -If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to -worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be -anything). - -=== "/etc/ntfy/server.yml" - ``` yaml - smtp-server-listen: ":25" - smtp-server-domain: "example.com" - smtp-server-addr-prefix: "ntfy-" # optional - ``` - -Then, in the email settings of your internal service, set the SMTP server address to the IP address of your -ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password -fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter. -In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or -`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`). - -So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts` -topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need -to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`. - -If the internal service lets you use define an email "Subject", it will become the title of the notification. -The body of the email will become the message of the notification. - ## Behind a proxy (TLS, etc.) !!! warning If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are @@ -682,8 +649,8 @@ or the root domain: ServerName ntfy.sh - # Proxy connections to ntfy (requires "a2enmod proxy proxy_http") - ProxyPass / http://127.0.0.1:2586/ upgrade=websocket + # Proxy connections to ntfy (requires "a2enmod proxy") + ProxyPass / http://127.0.0.1:2586/ ProxyPassReverse / http://127.0.0.1:2586/ SetEnv proxy-nokeepalive 1 @@ -691,13 +658,19 @@ or the root domain: # Higher than the max message size of 4096 bytes LimitRequestBody 102400 + + # Enable mod_rewrite (requires "a2enmod rewrite") + RewriteEngine on + + # WebSockets support (requires "a2enmod rewrite proxy_wstunnel") + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L] # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want - # it to work with curl without the annoying https:// prefix (requires "a2enmod alias") - - RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1" - - + # it to work with curl without the annoying https:// prefix + RewriteCond %{REQUEST_METHOD} GET + RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L] @@ -708,8 +681,8 @@ or the root domain: SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem Include /etc/letsencrypt/options-ssl-apache.conf - # Proxy connections to ntfy (requires "a2enmod proxy proxy_http") - ProxyPass / http://127.0.0.1:2586/ upgrade=websocket + # Proxy connections to ntfy (requires "a2enmod proxy") + ProxyPass / http://127.0.0.1:2586/ ProxyPassReverse / http://127.0.0.1:2586/ SetEnv proxy-nokeepalive 1 @@ -717,7 +690,14 @@ or the root domain: # Higher than the max message size of 4096 bytes LimitRequestBody 102400 - + + # Enable mod_rewrite (requires "a2enmod rewrite") + RewriteEngine on + + # WebSockets support (requires "a2enmod rewrite proxy_wstunnel") + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L] ``` @@ -809,57 +789,6 @@ Note that the self-hosted server literally sends the message `New message` for e may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all), it'll show `New message` as a popup. -## Web Push -[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030)) -allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed. -When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the -user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then -forward it to the browser. - -To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`), -a database to keep track of the browser's subscriptions, and an admin email address (you): - -- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 -- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 -- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` -- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com` -- `web-push-startup-queries` is an optional list of queries to run on startup` - -Limitations: - -- Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A _valid_ - certificate is required, as service workers will not run on origins with untrusted certificates. - -- Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This - is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin. - -To configure VAPID keys, first generate them: - -```sh -$ ntfy webpush keys -Web Push keys generated. -... -``` - -Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments: - -```yaml -web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 -web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890 -web-push-file: /var/cache/ntfy/webpush.db -web-push-email-address: sysadmin@example.com -``` - -The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days, -and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed), -subscriptions are also removed automatically. - -The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription -file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then. - -Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and -if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission. - ## Tiers ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments), @@ -1180,10 +1109,10 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a ## Health checks A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below. -If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy. +If a non-200 HTTP status code is returned or if the returned `health` field is `false` the ntfy service should be considered as unhealthy. ```json -{"healthy":true} +{"health":true} ``` See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment. @@ -1356,17 +1285,13 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | -| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate | -| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate | -| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions | -| `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: `(smh)`, e.g. 30s, 20m or 1h. The format for a *size* is: `(GMK)`, e.g. 1G, 200M or 4000k. ## Command line options ``` +$ ntfy serve --help NAME: ntfy serve - Run the ntfy server @@ -1396,8 +1321,8 @@ OPTIONS: --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] --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] + --listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP] + --listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS] --listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX] --listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE] --key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE] @@ -1418,12 +1343,11 @@ OPTIONS: --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] --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] + --web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT] --enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP] --enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN] --enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS] --upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL] - --upstream-access-token value, --upstream_access_token value access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN] --smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR] --smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER] --smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS] @@ -1431,10 +1355,6 @@ OPTIONS: --smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN] --smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN] --smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX] - --twilio-account value, --twilio_account value Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT] - --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] --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] @@ -1445,18 +1365,10 @@ OPTIONS: --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-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] --stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY] - --billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT] - --enable-metrics, --enable_metrics if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS] - --metrics-listen-http value, --metrics_listen_http value ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP] - --profile-listen-http value, --profile_listen_http value ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP] - --web-push-public-key value, --web_push_public_key value public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY] - --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] - --help, -h show help + --billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT] + --help, -h show help (default: false) ``` + diff --git a/docs/develop.md b/docs/develop.md index b090c8c5..a53c5033 100644 --- a/docs/develop.md +++ b/docs/develop.md @@ -16,7 +16,7 @@ server consists of three components: * **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/), which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to build the docs. -* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/) +* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/) to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`) and install all the 100,000 dependencies (*sigh*). @@ -163,15 +163,6 @@ $ make release-snapshot During development, you may want to be more picky and build only certain things. Here are a few examples. -### Build a Docker image only for Linux - -This is useful to test the final build with web app, docs, and server without any dependencies locally - -``` shell -$ make docker-dev -$ docker run --rm -p 80:80 binwiederhier/ntfy:dev serve -``` - ### Build the ntfy binary To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets: @@ -241,41 +232,6 @@ $ cd web $ npm start ``` -### Testing Web Push locally - -Reference: - -#### With the dev servers - -1. Get web push keys `go run main.go webpush keys` - -2. Run the server with web push enabled - - ```sh - go run main.go \ - --log-level debug \ - serve \ - --web-push-public-key KEY \ - --web-push-private-key KEY \ - --web-push-email-address \ - --web-push-file=/tmp/webpush.db - ``` - -3. In `web/public/config.js`: - - - Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`. - - - Set the `web_push_public_key` correctly. - -4. Run `npm run start` - -#### With a built package - -1. Run `make web-build` - -2. Run the server (step 2 above) - -3. Open ### Build the docs The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the documentation. As long as you have `mkdocs` installed (see above), this should work fine: @@ -429,7 +385,7 @@ steps: ### XCode setup -1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the +1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the `firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging 1. Similarly, install the SQLite.swift package dependency in XCode 1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators diff --git a/docs/emojis.md b/docs/emojis.md index d801ae09..fa01bb47 100644 --- a/docs/emojis.md +++ b/docs/emojis.md @@ -2,9 +2,9 @@ -You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically +You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the -[tagging and emojis page](publish.md#tags-emojis). +[tagging and emojis page](../publish/#tags-emojis). diff --git a/docs/examples.md b/docs/examples.md index 4e936d91..8164e2bf 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -135,21 +135,6 @@ You can send a message during a workflow run with curl. Here is an example sendi ${{ secrets.NTFY_URL }} ``` -## Changedetection.io -ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop), -[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io)) -uses [apprise](https://github.com/caronc/apprise) library for notification integrations. - -To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy) -to the notification list. - -For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}` - -In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add -the special ntfy Apprise Notification URL to the Notification List. - -![ntfy alerts on website change](static/img/cdio-setup.jpg) - ## Watchtower (shoutrrr) You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send [Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic. diff --git a/docs/faq.md b/docs/faq.md index 6ff97cfe..d7977a5f 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -76,29 +76,7 @@ However, if you still want to disable it, you can do so with the `web-root: disa Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless. -## If topic names are public, could I not just brute force them? -If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you -choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name, -the topic is as good as a good password. - -As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited. -In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you -choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you -could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name. - -For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly. - ## Where can I donate? I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier). I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much appreciated. - -## Can I email you? Can I DM you on Discord/Matrix? -While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org), -[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally -**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a -[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities. - -I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions -in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users -may be able to help out. I hope you understand. diff --git a/docs/install.md b/docs/install.md index c1a621d7..1d284956 100644 --- a/docs/install.md +++ b/docs/install.md @@ -14,15 +14,14 @@ We support amd64, armv7 and arm64. 1. Install ntfy using one of the methods described below 2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)) -3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml)) +3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml)) To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm). To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md) for details). -If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or -[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great -resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._ +If you like video tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU). +It's short and to the point. _I am not affiliated with Kris, I just liked the video._ ## Linux binaries Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and @@ -30,37 +29,37 @@ deb/rpm packages. === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.tar.gz - tar zxvf ntfy_2.7.0_linux_amd64.tar.gz - sudo cp -a ntfy_2.7.0_linux_amd64/ntfy /usr/local/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_amd64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_x86_64.tar.gz + tar zxvf ntfy_2.5.0_linux_x86_64.tar.gz + sudo cp -a ntfy_2.5.0_linux_x86_64/ntfy /usr/local/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_x86_64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.tar.gz - tar zxvf ntfy_2.7.0_linux_armv6.tar.gz - sudo cp -a ntfy_2.7.0_linux_armv6/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv6/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.tar.gz + tar zxvf ntfy_2.5.0_linux_armv6.tar.gz + sudo cp -a ntfy_2.5.0_linux_armv6/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv6/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.tar.gz - tar zxvf ntfy_2.7.0_linux_armv7.tar.gz - sudo cp -a ntfy_2.7.0_linux_armv7/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv7/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.tar.gz + tar zxvf ntfy_2.5.0_linux_armv7.tar.gz + sudo cp -a ntfy_2.5.0_linux_armv7/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv7/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.tar.gz - tar zxvf ntfy_2.7.0_linux_arm64.tar.gz - sudo cp -a ntfy_2.7.0_linux_arm64/ntfy /usr/bin/ntfy - sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_arm64/{client,server}/*.yml /etc/ntfy + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.tar.gz + tar zxvf ntfy_2.5.0_linux_arm64.tar.gz + sudo cp -a ntfy_2.5.0_linux_arm64/ntfy /usr/bin/ntfy + sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_arm64/{client,server}/*.yml /etc/ntfy sudo ntfy serve ``` @@ -110,7 +109,7 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -118,7 +117,7 @@ Manually installing the .deb file: === "armv6" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -126,7 +125,7 @@ Manually installing the .deb file: === "armv7/armhf" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -134,7 +133,7 @@ Manually installing the .deb file: === "arm64" ```bash - wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.deb + wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.deb sudo dpkg -i ntfy_*.deb sudo systemctl enable ntfy sudo systemctl start ntfy @@ -144,36 +143,34 @@ Manually installing the .deb file: === "x86_64/amd64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv6" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "armv7/armhf" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` === "arm64" ```bash - sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.rpm + sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.rpm sudo systemctl enable ntfy sudo systemctl start ntfy ``` ## Arch Linux -ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). -You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, -build and install ntfy and keep it up to date. +ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date. ``` paru -S ntfysh-bin ``` @@ -195,18 +192,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. -To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz), +To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_macOS_all.tar.gz), extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). ```bash -curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_darwin_all.tar.gz > ntfy_2.7.0_darwin_all.tar.gz -tar zxvf ntfy_2.7.0_darwin_all.tar.gz -sudo cp -a ntfy_2.7.0_darwin_all/ntfy /usr/local/bin/ntfy +curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_macOS_all.tar.gz > ntfy_2.5.0_macOS_all.tar.gz +tar zxvf ntfy_2.5.0_macOS_all.tar.gz +sudo cp -a ntfy_2.5.0_macOS_all/ntfy /usr/local/bin/ntfy mkdir ~/Library/Application\ Support/ntfy -cp ntfy_2.7.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml +cp ntfy_2.5.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml ntfy --help ``` @@ -224,7 +221,7 @@ brew install ntfy ## Windows The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. -To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_windows_amd64.zip), +To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_windows_x86_64.zip), extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). @@ -280,7 +277,7 @@ docker run \ Using docker-compose with non-root user and healthchecks enabled: ```yaml -version: "2.3" +version: "2.1" services: ntfy: diff --git a/docs/integrations.md b/docs/integrations.md index a8ffa6a9..d1a4d42c 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -23,8 +23,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python - [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier - [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server -- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring -- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification ## Integration via HTTP/SMTP/etc. @@ -57,9 +55,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi) - [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS) - [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart) -- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go) -- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP) -- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java) ## CLIs + GUIs @@ -126,38 +121,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig) - [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript) - [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS) -- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup and shutdown (Go) -- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash) -- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP) -- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager -- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python) -- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly. -- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice. -- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes -- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS -- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell) ## Blog + forum posts -- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023 -- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023 -- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023 -- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023 -- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023 -- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023 -- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023 -- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023 -- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023 -- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023 -- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023 -- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023 -- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023 -- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023 -- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023 -- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023 -- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023 -- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023 -- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023 - [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023 - [桌面通知:ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023 - [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023 @@ -181,7 +147,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had - [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022 - [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022 - [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022 -- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022 - [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022 - [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022 - [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022 @@ -238,7 +203,6 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!** | [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany | | [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany | | [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France | -| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany | Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability and uptime of third party servers, so use of each server is **at your own discretion**. diff --git a/docs/known-issues.md b/docs/known-issues.md index cdb95bb6..f0528422 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -1,5 +1,5 @@ # Known issues -This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete +This is an incomplete list of known issues with the ntfy server, Android app, and iOS app. You can find a complete list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful to have the prominent ones here to link to. @@ -26,18 +26,3 @@ Be sure that in your selfhosted server: * Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**) * Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to - -## iOS app seeing "New message", but not real message content -If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that -your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server. - -Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)), -and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications). - -## Safari does not play sounds for web push notifications -Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with -iOS 17 / Safari 17, which will be released later in 2023. - -## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787)) -When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a -[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found. diff --git a/docs/publish.md b/docs/publish.md index 41370778..11e33e61 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -138,7 +138,7 @@ a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an Tags = "warning,skull" } Body = "Remote access to phils-laptop detected. Act right away." - } + } Invoke-RestMethod @Request ``` @@ -457,7 +457,6 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P === "PowerShell" ``` powershell $Request = @{ - Method = 'POST' URI = "https://ntfy.sh/phil_alerts" Headers = @{ Priority = "5" @@ -624,109 +623,6 @@ them with a comma, e.g. `tag1,tag2,tag3`. as [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2), e.g. `tag1,=?UTF-8?B?8J+HqfCfh6o=?=` ([base64](https://en.wikipedia.org/wiki/Base64)), or `=?UTF-8?Q?=C3=84pfel?=,tag2` ([quoted-printable](https://en.wikipedia.org/wiki/Quoted-printable)). -## Markdown formatting -_Supported on:_ :material-firefox: - -You can format messages using [Markdown](https://www.markdownguide.org/basic-syntax/) 🤩. That means you can use -**bold text**, *italicized text*, links, images, and more. Supported Markdown features (web app only for now): - -- [Emphasis](https://www.markdownguide.org/basic-syntax/#emphasis) such as **bold** (`**bold**`), *italics* (`*italics*`) -- [Links](https://www.markdownguide.org/basic-syntax/#links) (`[some tool](https://ntfy.sh)`) -- [Images](https://www.markdownguide.org/basic-syntax/#images) (`![some image](https://bing.com/logo.png)`) -- [Code blocks](https://www.markdownguide.org/basic-syntax/#code-blocks) (` ```code blocks``` `) and [inline code](https://www.markdownguide.org/basic-syntax/#inline-code) (`` `inline code` ``) -- [Headings](https://www.markdownguide.org/basic-syntax/#headings) (`# headings`, `## headings`, etc.) -- [Lists](https://www.markdownguide.org/basic-syntax/#lists) (`- lists`, `1. lists`, etc.) -- [Blockquotes](https://www.markdownguide.org/basic-syntax/#blockquotes) (`> blockquotes`) -- [Horizontal rules](https://www.markdownguide.org/basic-syntax/#horizontal-rules) (`---`) - -By default, messages sent to ntfy are rendered as plain text. To enable Markdown, set the `X-Markdown` header (or any of -its aliases: `Markdown`, or `md`) to `true` (or `1` or `yes`), or set the `Content-Type` header to `text/markdown`. -As of today, **Markdown is only supported in the web app.** Here's an example of how to enable Markdown formatting: - -=== "Command line (curl)" - ``` - curl \ - -d "Look ma, **bold text**, *italics*, ..." \ - -H "Markdown: yes" \ - ntfy.sh/mytopic - ``` - -=== "ntfy CLI" - ``` - ntfy publish \ - --markdown \ - mytopic \ - "Look ma, **bold text**, *italics*, ..." - ``` - -=== "HTTP" - ``` http - POST /mytopic HTTP/1.1 - Host: ntfy.sh - Markdown: yes - - Look ma, **bold text**, *italics*, ... - ``` - -=== "JavaScript" - ``` javascript - fetch('https://ntfy.sh/mytopic', { - method: 'POST', // PUT works too - body: 'Look ma, **bold text**, *italics*, ...', - headers: { 'Markdown': 'yes' } - }) - ``` - -=== "Go" - ``` go - http.Post("https://ntfy.sh/mytopic", "text/markdown", - strings.NewReader("Look ma, **bold text**, *italics*, ...")) - - // or - req, _ := http.NewRequest("POST", "https://ntfy.sh/mytopic", - strings.NewReader("Look ma, **bold text**, *italics*, ...")) - req.Header.Set("Markdown", "yes") - http.DefaultClient.Do(req) - ``` - -=== "PowerShell" - ``` powershell - $Request = @{ - Method = "POST" - URI = "https://ntfy.sh/mytopic" - Body = "Look ma, **bold text**, *italics*, ..." - Headers = @{ - Markdown = "yes" - } - } - Invoke-RestMethod @Request - ``` - -=== "Python" - ``` python - requests.post("https://ntfy.sh/mytopic", - data="Look ma, **bold text**, *italics*, ..." - headers={ "Markdown": "yes" })) - ``` - -=== "PHP" - ``` php-inline - file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ - 'http' => [ - 'method' => 'POST', // PUT also works - 'header' => 'Content-Type: text/markdown', // ! - 'content' => 'Look ma, **bold text**, *italics*, ...' - ] - ])); - ``` - -Here's what that looks like in the web app: - -
- ![markdown](static/img/web-markdown.png){ width=500 } -
Markdown formatting in the web app
-
- ## Scheduled delivery _Supported on:_ :material-android: :material-apple: :material-firefox: @@ -1034,7 +930,7 @@ is the only required one: $Request = @{ Method = "POST" URI = "https://ntfy.sh" - Body = ConvertTo-JSON @{ + Body = @{ Topic = "mytopic" Title = "Low disk space alert" Message = "Disk space is low at 5.1 GB" @@ -1043,7 +939,7 @@ is the only required one: FileName = "diskspace.jpg" Tags = @("warning", "cd") Click = "https://homecamera.lan/xasds1h2xsSsa/" - Actions = @( + Actions = ConvertTo-JSON @( @{ Action = "view" Label = "Admin panel" @@ -1108,7 +1004,6 @@ all the supported fields: | `actions` | - | *JSON array* | *(see [action buttons](#action-buttons))* | Custom [user action buttons](#action-buttons) for notifications | | `click` | - | *URL* | `https://example.com` | Website opened when notification is [clicked](#click-action) | | `attach` | - | *URL* | `https://example.com/file.jpg` | URL of an attachment, see [attach via URL](#attach-file-from-url) | -| `markdown` | - | *bool* | `true` | Set to true if the `message` is Markdown-formatted | | `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | | `filename` | - | *string* | `file.jpg` | File name of the attachment | | `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | @@ -1131,7 +1026,7 @@ As of today, the following actions are supported: when the action button is tapped (only supported on Android) * [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped -Here's an example of what a notification with actions can look like: +Here's an example of what that a notification with actions can look like:
![notification with actions](static/img/android-screenshot-notification-actions.png){ width=500 } @@ -1920,10 +1815,10 @@ And the same example using [JSON publishing](#publish-as-json): $Request = @{ Method = "POST" URI = "https://ntfy.sh" - Body = ConvertTo-Json -Depth 3 @{ + Body = @{ Topic = "wifey" Message = "Your wife requested you send a picture of yourself." - Actions = @( + Actions = ConvertTo-Json -Depth 3 @( @{ Action = "broadcast" Label = "Take picture" @@ -2073,7 +1968,7 @@ Here's an example using the [`X-Actions` header](#using-a-header): 'method' => 'POST', 'header' => "Content-Type: text/plain\r\n" . - 'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}', + "Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}", 'content' => 'Garage door has been open for 15 minutes. Close it?' ] ])); @@ -2200,10 +2095,10 @@ And the same example using [JSON publishing](#publish-as-json): $Request = @{ Method = "POST" URI = "https://ntfy.sh" - Body = ConvertTo-Json -Depth 3 @{ + Body = @{ Topic = "myhome" Message = "Garage door has been open for 15 minutes. Close it?" - Actions = @( + Actions = ConvertTo-Json -Depth 3 @( @{ Action = "http" Label = "Close door" @@ -2288,7 +2183,7 @@ You can define which URL to open when a notification is clicked. This may be use to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open the web browser (or the app) and open the website. -To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its alias `Click`). +To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`). If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled by another app, the responsible app may open. @@ -3598,7 +3493,6 @@ table in their canonical form. | `X-Actions` | `Actions`, `Action` | JSON array or short format of [user actions](#action-buttons) | | `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) | | `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment | -| `X-Markdown` | `Markdown`, `md` | Enable [Markdown formatting](#markdown-formatting) in the notification body | | `X-Icon` | `Icon` | URL to use as notification [icon](#icons) | | `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | @@ -3608,4 +3502,3 @@ table in their canonical form. | `X-UnifiedPush` | `UnifiedPush`, `up` | [UnifiedPush](#unifiedpush) publish option, only to be used by UnifiedPush apps | | `X-Poll-ID` | `Poll-ID` | Internal parameter, used for [iOS push notifications](config.md#ios-instant-notifications) | | `Authorization` | - | If supported by the server, you can [login to access](#authentication) protected topics | -| `Content-Type` | - | If set to `text/markdown`, [Markdown formatting](#markdown-formatting) is enabled | diff --git a/docs/releases.md b/docs/releases.md index 73e5eb20..71fceb1e 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,85 +2,6 @@ Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases). -## ntfy server v2.7.0 -Released August 17, 2023 - -This release ships Markdown support for the web app (not in the Android app yet), and adds support for -right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting, -internationalization support, a CLI auth bug. - -Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users -in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a -single user to deny access to all other users of a ntfy instance**. Please note that while tokens were -erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838) -for details. **Please upgrade your ntfy instance if you run a multi-user system.** - -**Features:** - -* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves)) -* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost)) - -**Security:** ⚠️ - -* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838)) - -**Bug fixes + maintenance:** - -* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves)) -* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing) - -**Documentation:** - -* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard)) - -## ntfy server v2.6.2 -Released June 30, 2023 - -With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA) -with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar -to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window, -push notifications, and an app badge with the unread notification count. Note that for self-hosted servers, -[Web Push](config.md#web-push) must be configured. - -On top of that, this release also brings **dark mode** 🧛🌙 to the web app. - -🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the -Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions. - -❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier) -and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off -if you use promo code `MYTOPIC`). ntfy will always remain open source. - -**Features:** - -* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost)) - -**Bug fixes:** - -* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting) -* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting) -* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting) -* Newly created access tokens are now lowercase only to fully support `+@` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting) -* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost)) - -**Maintenance:** - -* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost)) -* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost)) - -**Changes in tarball/zip naming:** -Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release -archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy: - -- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip` -- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz` -- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz` - ## ntfy server v2.5.0 Released May 18, 2023 @@ -110,7 +31,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source. ## ntfy server v2.4.0 Released Apr 26, 2023 -This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`, +This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds suport to encode the `X-Title`, `X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website. ❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier) @@ -1273,7 +1194,7 @@ Released Dec 28, 2021 **Features & bug fixes:** -* [Publish messages via e-mail](publish.md#e-mail-publishing) #66 +* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66 * Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64 * Fixing the Santa bug #65 @@ -1283,16 +1204,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release ## Not released yet -### ntfy server v2.8.0 (UNRELEASED) - -**Bug fixes + maintenance:** - -* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting) -* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307)) -* Add special logic to ignore `Priority` header if it resembled a RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461)) -* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves)) -* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero)) - ### ntfy Android app v1.16.1 (UNRELEASED) **Features:** @@ -1308,3 +1219,16 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Additional languages:** * Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/hellbown/)) + +### ntfy server v2.6.0 (UNRELEASED) + +**Bug fixes:** + +* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting) + +**Maintenance:** + +* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost)) +* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost)) +* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost)) +* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost)) diff --git a/docs/static/img/cdio-setup.jpg b/docs/static/img/cdio-setup.jpg deleted file mode 100644 index 2f9e44cb..00000000 Binary files a/docs/static/img/cdio-setup.jpg and /dev/null differ diff --git a/docs/static/img/pwa-badge.png b/docs/static/img/pwa-badge.png deleted file mode 100644 index 1a22de07..00000000 Binary files a/docs/static/img/pwa-badge.png and /dev/null differ diff --git a/docs/static/img/pwa-install-chrome-android-menu.jpg b/docs/static/img/pwa-install-chrome-android-menu.jpg deleted file mode 100644 index 1c258d64..00000000 Binary files a/docs/static/img/pwa-install-chrome-android-menu.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-chrome-android-popup.jpg b/docs/static/img/pwa-install-chrome-android-popup.jpg deleted file mode 100644 index 90c77c77..00000000 Binary files a/docs/static/img/pwa-install-chrome-android-popup.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-chrome-android.jpg b/docs/static/img/pwa-install-chrome-android.jpg deleted file mode 100644 index 7f89503f..00000000 Binary files a/docs/static/img/pwa-install-chrome-android.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-firefox-android-menu.jpg b/docs/static/img/pwa-install-firefox-android-menu.jpg deleted file mode 100644 index c0aa59e7..00000000 Binary files a/docs/static/img/pwa-install-firefox-android-menu.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-firefox-android-popup.jpg b/docs/static/img/pwa-install-firefox-android-popup.jpg deleted file mode 100644 index e97a858d..00000000 Binary files a/docs/static/img/pwa-install-firefox-android-popup.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-macos-safari-add-to-dock.png b/docs/static/img/pwa-install-macos-safari-add-to-dock.png deleted file mode 100644 index 8a780605..00000000 Binary files a/docs/static/img/pwa-install-macos-safari-add-to-dock.png and /dev/null differ diff --git a/docs/static/img/pwa-install-safari-ios-add-icon.jpg b/docs/static/img/pwa-install-safari-ios-add-icon.jpg deleted file mode 100644 index 175fb8b4..00000000 Binary files a/docs/static/img/pwa-install-safari-ios-add-icon.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-safari-ios-button.jpg b/docs/static/img/pwa-install-safari-ios-button.jpg deleted file mode 100644 index c9897c30..00000000 Binary files a/docs/static/img/pwa-install-safari-ios-button.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install-safari-ios-menu.jpg b/docs/static/img/pwa-install-safari-ios-menu.jpg deleted file mode 100644 index b6408afd..00000000 Binary files a/docs/static/img/pwa-install-safari-ios-menu.jpg and /dev/null differ diff --git a/docs/static/img/pwa-install.png b/docs/static/img/pwa-install.png deleted file mode 100644 index c44e7dbc..00000000 Binary files a/docs/static/img/pwa-install.png and /dev/null differ diff --git a/docs/static/img/pwa.png b/docs/static/img/pwa.png deleted file mode 100644 index c26f29f1..00000000 Binary files a/docs/static/img/pwa.png and /dev/null differ diff --git a/docs/static/img/web-markdown.png b/docs/static/img/web-markdown.png deleted file mode 100644 index 612f2cf3..00000000 Binary files a/docs/static/img/web-markdown.png and /dev/null differ diff --git a/docs/static/img/web-pin.png b/docs/static/img/web-pin.png new file mode 100644 index 00000000..3312a50f Binary files /dev/null and b/docs/static/img/web-pin.png differ diff --git a/docs/static/img/web-subscribe.png b/docs/static/img/web-subscribe.png index ccbd0493..f60a8658 100644 Binary files a/docs/static/img/web-subscribe.png and b/docs/static/img/web-subscribe.png differ diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 7f589d3c..59cfc8e7 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c ## Install + configure To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client -by creating `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user). You +by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub. If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**, diff --git a/docs/subscribe/phone.md b/docs/subscribe/phone.md index e88ff0fb..440dbbe3 100644 --- a/docs/subscribe/phone.md +++ b/docs/subscribe/phone.md @@ -12,9 +12,6 @@ You can get the Android app from both [Google Play](https://play.google.com/stor from [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/). Both are largely identical, with the one exception that the F-Droid flavor does not use Firebase. The iOS app can be downloaded from the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347). -Alternatively, you may also want to consider using the **[progressive web app (PWA)](pwa.md)** instead of the native app. -The PWA is a website that you can add to your home screen, and it will behave just like a native app. - ## Overview A picture is worth a thousand words. Here are a few screenshots showing what the app looks like. It's all pretty straight forward. You can add topics and as soon as you add them, you can [publish messages](../publish.md) to them. diff --git a/docs/subscribe/pwa.md b/docs/subscribe/pwa.md deleted file mode 100644 index 5dcaa257..00000000 --- a/docs/subscribe/pwa.md +++ /dev/null @@ -1,69 +0,0 @@ -# Using the progressive web app (PWA) -While ntfy doesn't have a native desktop app, it is built as a [progressive web app](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) (PWA) -and thus can be **installed on both desktop and mobile devices**. - -This gives it its own launcher (e.g. shortcut on Windows, app on macOS, launcher shortcut on Linux, home screen icon on iOS, and -launcher icon on Android), a standalone window, push notifications, and an app badge with the unread notification count. - -Web app installation is **supported on** (see [compatibility table](https://caniuse.com/web-app-manifest) for details): - -- **Chrome:** Android, Windows, Linux, macOS -- **Safari:** iOS 16.4+, macOS 14+ -- **Firefox:** Android, as well as on Windows/Linux [via an extension](https://addons.mozilla.org/en-US/firefox/addon/pwas-for-firefox/) -- **Edge:** Windows - -Note that for self-hosted servers, [Web Push](../config.md#web-push) must be configured for the PWA to work. - -## Installation - -### Chrome on Desktop -To install and register the web app via Chrome, click the "install app" icon. After installation, you can find the app in your -app drawer: - -
- - - -
- -### Safari on macOS -To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher. - -
- -
- -### Chrome/Firefox on Android -For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app" -in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer, -and on your home screen. - -
- - - -
- -For Firefox, select "Install" in the menu, and then click "Add" to add an icon to your home screen: - -
- - -
- -### Safari on iOS -On iOS Safari, tap on the Share menu, then tap "Add to Home Screen": - -
- - - -
- -## Background notifications -Background notifications via web push are enabled by default and cannot be turned off when the app is installed, as notifications would -not be delivered reliably otherwise. You can mute topics you don't want to receive notifications for. - -On desktop, you generally need either your browser or the web app open to receive notifications, though the ntfy tab doesn't need to be -open. On mobile, you don't need to have the web app open to receive notifications. Look at the [web docs](./web.md#background-notifications) -for a detailed breakdown. diff --git a/docs/subscribe/web.md b/docs/subscribe/web.md index 859f7d0a..5c2672f0 100644 --- a/docs/subscribe/web.md +++ b/docs/subscribe/web.md @@ -1,75 +1,27 @@ -# Subscribe from the web app -The web app lets you subscribe and publish messages to ntfy topics. For ntfy.sh, the web app is available at [ntfy.sh/app](https://ntfy.sh/app). -To subscribe, simply type in the topic name and click the *Subscribe* button. **After subscribing, messages published to the topic -will appear in the web app, and pop up as a notification.** +# Subscribe from the Web UI +You can use the Web UI to subscribe to topics as well. If you do, and you keep the website open, **notifications will +pop up as desktop notifications**. Simply type in the topic name and click the *Subscribe* button. The browser will +keep a connection open and listen for incoming notifications. -
- -
- -## Publish messages To learn how to send messages, check out the [publishing page](../publish.md).
+
-## Topic reservations +To keep receiving desktop notifications from ntfy, you need to keep the website open. What I do, and what I highly recommend, +is to pin the tab so that it's always open, but sort of out of the way: + +
+ ![pinned](../static/img/web-pin.png){ width=500 } +
Pin web app to move it out of the way
+
+ If topic reservations are enabled, you can claim ownership over topics and define access to it:
- -## Notification features and browser support - -- Emoji tags are supported in all browsers - -- [Click](../publish.md#click-action) actions are supported in all browsers - -- Only Chrome, Edge, and Opera support displaying view and http [actions](../publish.md#action-buttons) in notifications. - - Their presentation is platform specific. - - Note that HTTP actions are performed using fetch and thus are limited to the [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - rules, which means that any URL you include needs to respond to a [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) - with headers allowing the origin of the ntfy web app (`Access-Control-Allow-Origin: https://ntfy.sh`) or `*`. - -- Only Chrome, Edge, and Opera support displaying [images](../publish.md#attachments) in notifications. - -Look at the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API#browser_compatibility) -for more info. - -## Background notifications -While subscribing, you have the option to enable background notifications on supported browsers (see "Settings" tab). - -Note: If you add the web app to your homescreen (as a progressive web app, more info in the [installed web app](pwa.md) -docs), you cannot turn these off, as notifications would not be delivered reliably otherwise. You can mute topics you don't want to receive -notifications for. - -**If background notifications are off:** This requires an active ntfy tab to be open to receive notifications. -These are typically instantaneous, and will appear as a system notification. If you don't see these, check that your browser -is allowed to show notifications (for example in System Settings on macOS). If you don't want to enable background notifications, -**pinning the ntfy tab on your browser** is a good solution to leave it running. - -**If background notifications are on:** This uses the [Web Push API](https://caniuse.com/push-api). You don't need an active -ntfy tab open, but in some cases you may need to keep your browser open. Background notifications are only supported on the -same server hosting the web app. You cannot use another server, but can instead subscribe on the other server itself. - -If the ntfy app is not opened for more than a week, background notifications will be paused. You can resume them -by opening the app again, and will get a warning notification before they are paused. - -| Browser | Platform | Browser Running | Browser Not Running | Restrictions | -|---------|----------|-----------------|---------------------|---------------------------------------------------------| -| Chrome | Desktop | ✅ | ❌ | | -| Firefox | Desktop | ✅ | ❌ | | -| Edge | Desktop | ✅ | ❌ | | -| Opera | Desktop | ✅ | ❌ | | -| Safari | Desktop | ✅ | ✅ | requires Safari 16.1, macOS 13 Ventura | -| Chrome | Android | ✅ | ✅ | | -| Firefox | Android | ✅ | ✅ | | -| Safari | iOS | ⚠️ | ⚠️ | requires iOS 16.4, only when app is added to homescreen | - -(Browsers below 1% usage not shown, look at the [Push API](https://caniuse.com/push-api) for more info) diff --git a/go.mod b/go.mod index a7395d5b..162fd943 100644 --- a/go.mod +++ b/go.mod @@ -1,79 +1,74 @@ -module git.zio.sh/astra/ntfy/v2 +module heckel.io/ntfy go 1.18 require ( - cloud.google.com/go/firestore v1.14.0 // indirect - cloud.google.com/go/storage v1.34.1 // 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/olebedev/when v1.0.0 + cloud.google.com/go/firestore v1.9.0 // indirect + cloud.google.com/go/storage v1.30.1 // indirect + github.com/BurntSushi/toml v1.2.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/emersion/go-smtp v0.16.0 + github.com/gabriel-vasile/mimetype v1.4.2 + github.com/gorilla/websocket v1.5.0 + github.com/mattn/go-sqlite3 v1.14.16 + github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 github.com/stretchr/testify v1.8.1 - github.com/urfave/cli/v2 v2.25.7 - golang.org/x/crypto v0.14.0 - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sync v0.5.0 - golang.org/x/term v0.13.0 - golang.org/x/time v0.4.0 - google.golang.org/api v0.149.0 + github.com/urfave/cli/v2 v2.25.3 + golang.org/x/crypto v0.9.0 + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sync v0.2.0 + golang.org/x/term v0.8.0 + golang.org/x/time v0.3.0 + google.golang.org/api v0.122.0 gopkg.in/yaml.v2 v2.4.0 ) -replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pin version due to breaking changes, see #839 - require github.com/pkg/errors v0.9.1 // indirect require ( - firebase.google.com/go/v4 v4.12.1 - github.com/SherClockHolmes/webpush-go v1.3.0 - github.com/prometheus/client_golang v1.17.0 - github.com/stripe/stripe-go/v74 v74.30.0 + firebase.google.com/go/v4 v4.11.0 + github.com/prometheus/client_golang v1.15.1 + github.com/stripe/stripe-go/v74 v74.18.0 ) require ( - cloud.google.com/go v0.110.10 // indirect - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go v0.110.2 // indirect + cloud.google.com/go/compute v1.19.3 // 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.0.1 // indirect + cloud.google.com/go/longrunning v0.4.2 // indirect github.com/AlekSi/pointer v1.2.0 // indirect github.com/MicahParks/keyfunc v1.9.0 // indirect github.com/beorn7/perks v1.0.1 // indirect 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-20220912192320-0145f2c60ead // 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/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/s2a-go v0.1.3 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.8.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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.4.0 // indirect + github.com/prometheus/common v0.43.0 // indirect + github.com/prometheus/procfs v0.9.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 go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.14.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-20231030173426-d783a09b4405 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine/v2 v2.0.3 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc v1.55.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d93f52d2..bfaf339d 100644 --- a/go.sum +++ b/go.sum @@ -1,38 +1,44 @@ 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.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= +cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= 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.34.1 h1:H2Af2dU5J0PF7A5B+ECFIce+RqxVnrVilO+cu0TS3MI= -cloud.google.com/go/storage v1.34.1/go.mod h1:VN1ElqqvR9adg1k9xlkUJ55cMOP1/QjnNNuT5xQL6dY= -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.9.0 h1:IBlRyxgGySXu5VuW0RgGFlTtLukSnNkpDiEOMkQkmpA= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= +cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/longrunning v0.4.2 h1:WDKiiNXFTaQ6qz/G8FCOkuY9kJmOJGY67wPUC1M2RbE= +cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +firebase.google.com/go/v4 v4.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk= +firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE= 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= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= -github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -40,16 +46,17 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI= -github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8= +github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 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/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -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/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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= @@ -59,13 +66,16 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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= @@ -78,41 +88,44 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -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/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE= +github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= 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/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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w= -github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8 h1:0uFGkScHef2Xd8g74BMHU1jFcnKEm0PzrPn4CluQ9FI= +github.com/olebedev/when v0.0.0-20221205223600-4d190b02b8d8/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E= 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.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= 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.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.43.0 h1:iq+BVjvYLei5f27wiuNiB1DN6DYQkp1c8Bx0Vykh5us= +github.com/prometheus/common v0.43.0/go.mod h1:NCvr5cQIh3Y/gy73/RdVtC9r8xxrxwJnB+2lB3BxrFc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -120,86 +133,86 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/stripe/stripe-go/v74 v74.18.0 h1:ImSIoaVkTUozHxa21AhwHYBjwc8fVSJJJB1Q7oaXzIw= +github.com/stripe/stripe-go/v74 v74.18.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw= +github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= +github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.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/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 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.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 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-20181221193216-37e7f081c4d4/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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 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/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 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= @@ -207,35 +220,35 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-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.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es= +google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= 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= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k= -google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine/v2 v2.0.3 h1:AyY/mipuqiyCIAqOevfmu5fMDc5/9P/QggWfCQYdkSA= +google.golang.org/appengine/v2 v2.0.3/go.mod h1:2Z0TTdcXxnHdXzmp8drrmOExUDM2WQgyT33c6JDUlJM= 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-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 h1:I6WNifs6pF9tNdSob2W24JtyxIYjzFB9qDlpUC76q+U= -google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= -google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405 h1:HJMDndgxest5n2y77fnErkM62iUsptE/H8p0dC2Huo4= -google.golang.org/genproto/googleapis/api v0.0.0-20231030173426-d783a09b4405/go.mod h1:oT32Z4o8Zv2xPQTg0pbVaPr0MPOH6f14RgXt7zfIpwg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 h1:AB/lmRny7e2pLhFEYIbl5qkDAUt2h0ZRO4wGPhZf+ik= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405/go.mod h1:67X1fPuzjcrkymZzZV1vvkFeTn2Rvc6lYF9MYFGCcwE= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 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.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 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.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= 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= @@ -247,11 +260,12 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 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= 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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/log/event.go b/log/event.go index c4521674..b4b8f59f 100644 --- a/log/event.go +++ b/log/event.go @@ -3,7 +3,7 @@ package log import ( "encoding/json" "fmt" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" "log" "os" "sort" diff --git a/main.go b/main.go index 6aea6fa2..5b1428d1 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,8 @@ package main import ( "fmt" - "git.zio.sh/astra/ntfy/v2/cmd" "github.com/urfave/cli/v2" + "heckel.io/ntfy/cmd" "os" "runtime" ) diff --git a/mkdocs.yml b/mkdocs.yml index 7b14ee0c..4a7db366 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,7 +82,6 @@ nav: - "Subscribing": - "From your phone": subscribe/phone.md - "From the Web app": subscribe/web.md - - "From the Desktop": subscribe/pwa.md - "From the CLI": subscribe/cli.md - "Using the API": subscribe/api.md - "Self-hosting": diff --git a/scripts/emoji-convert.sh b/scripts/emoji-convert.sh index 8cbe397b..61ad5f79 100755 --- a/scripts/emoji-convert.sh +++ b/scripts/emoji-convert.sh @@ -25,9 +25,9 @@ elif [[ "$1" == *.md ]]; then -You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically +You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the -[tagging and emojis page](publish.md#tags-emojis). +[tagging and emojis page](../publish/#tags-emojis).
" > "$1" diff --git a/server/actions.go b/server/actions.go index ce61395c..80065873 100644 --- a/server/actions.go +++ b/server/actions.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" "regexp" "strings" "unicode/utf8" diff --git a/server/config.go b/server/config.go index c7b09082..a876926e 100644 --- a/server/config.go +++ b/server/config.go @@ -1,11 +1,10 @@ package server import ( + "heckel.io/ntfy/user" "io/fs" "net/netip" "time" - - "git.zio.sh/astra/ntfy/v2/user" ) // Defines default config settings (excluding limits, see below) @@ -23,12 +22,6 @@ const ( DefaultStripePriceCacheDuration = 3 * time.Hour // Time to keep Stripe prices cached in memory before a refresh is needed ) -// Defines default Web Push settings -const ( - DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour - DefaultWebPushExpiryDuration = 9 * 24 * time.Hour -) - // Defines all global and per-visitor limits // - message size limit: the max number of bytes for a message // - total topic limit: max number of topics overall @@ -153,13 +146,6 @@ type Config struct { EnableMetrics bool AccessControlAllowOrigin string // CORS header field to restrict access from web clients Version string // injected by App - WebPushPrivateKey string - WebPushPublicKey string - WebPushFile string - WebPushEmailAddress string - WebPushStartupQueries string - WebPushExpiryDuration time.Duration - WebPushExpiryWarningDuration time.Duration } // NewConfig instantiates a default new server config @@ -241,11 +227,5 @@ func NewConfig() *Config { EnableReservations: false, AccessControlAllowOrigin: "*", Version: "", - WebPushPrivateKey: "", - WebPushPublicKey: "", - WebPushFile: "", - WebPushEmailAddress: "", - WebPushExpiryDuration: DefaultWebPushExpiryDuration, - WebPushExpiryWarningDuration: DefaultWebPushExpiryWarningDuration, } } diff --git a/server/config_test.go b/server/config_test.go index 23fbadf1..14f028f1 100644 --- a/server/config_test.go +++ b/server/config_test.go @@ -1,8 +1,8 @@ package server_test import ( - "git.zio.sh/astra/ntfy/v2/server" "github.com/stretchr/testify/assert" + "heckel.io/ntfy/server" "testing" ) diff --git a/server/errors.go b/server/errors.go index f3d0d6b1..eee916b5 100644 --- a/server/errors.go +++ b/server/errors.go @@ -3,7 +3,7 @@ package server import ( "encoding/json" "fmt" - "git.zio.sh/astra/ntfy/v2/log" + "heckel.io/ntfy/log" "net/http" ) @@ -114,9 +114,6 @@ var ( errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil} errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "delayed call notifications are not supported", "", nil} - errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil} - errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil} - errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} @@ -141,6 +138,5 @@ var ( errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil} - errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil} errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil} ) diff --git a/server/file_cache.go b/server/file_cache.go index 499cca16..c097aefb 100644 --- a/server/file_cache.go +++ b/server/file_cache.go @@ -3,8 +3,8 @@ package server import ( "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "io" "os" "path/filepath" diff --git a/server/file_cache_test.go b/server/file_cache_test.go index cdb534be..8f267a73 100644 --- a/server/file_cache_test.go +++ b/server/file_cache_test.go @@ -3,8 +3,8 @@ package server import ( "bytes" "fmt" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/util" "os" "strings" "testing" diff --git a/server/log.go b/server/log.go index 23fdf5d5..c638ed97 100644 --- a/server/log.go +++ b/server/log.go @@ -2,10 +2,10 @@ package server import ( "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" "github.com/emersion/go-smtp" "github.com/gorilla/websocket" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "net/http" "strings" "unicode/utf8" @@ -29,7 +29,6 @@ const ( tagResetter = "resetter" tagWebsocket = "websocket" tagMatrix = "matrix" - tagWebPush = "webpush" ) var ( diff --git a/server/message_cache.go b/server/message_cache.go index fafd6d9b..1d7302af 100644 --- a/server/message_cache.go +++ b/server/message_cache.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" _ "github.com/mattn/go-sqlite3" // SQLite driver + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) var ( @@ -45,7 +45,6 @@ const ( attachment_deleted INT NOT NULL, sender TEXT NOT NULL, user TEXT NOT NULL, - content_type TEXT NOT NULL, encoding TEXT NOT NULL, published INT NOT NULL ); @@ -64,43 +63,43 @@ const ( COMMIT; ` insertMessageQuery = ` - INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, content_type, encoding, published) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, user, encoding, published) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` deleteMessageQuery = `DELETE FROM messages WHERE mid = ?` updateMessagesForTopicExpiryQuery = `UPDATE messages SET expires = ? WHERE topic = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics selectMessagesByIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE mid = ? ` selectMessagesSinceTimeQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE topic = ? AND time >= ? AND published = 1 ORDER BY time, id ` selectMessagesSinceTimeIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE topic = ? AND time >= ? ORDER BY time, id ` selectMessagesSinceIDQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE topic = ? AND id > ? AND published = 1 ORDER BY time, id ` selectMessagesSinceIDIncludeScheduledQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE topic = ? AND (id > ? OR published = 0) ORDER BY time, id ` selectMessagesDueQuery = ` - SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, content_type, encoding + SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, user, encoding FROM messages WHERE time <= ? AND published = 0 ORDER BY time, id @@ -122,7 +121,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 12 + currentSchemaVersion = 11 createSchemaVersionTableQuery = ` CREATE TABLE IF NOT EXISTS schemaVersion ( id INT PRIMARY KEY, @@ -241,11 +240,6 @@ const ( ); INSERT INTO stats (key, value) VALUES ('messages', 0); ` - - // 11 -> 12 - migrate11To12AlterMessagesTableQuery = ` - ALTER TABLE messages ADD COLUMN content_type TEXT NOT NULL DEFAULT(''); - ` ) var ( @@ -261,7 +255,6 @@ var ( 8: migrateFrom8, 9: migrateFrom9, 10: migrateFrom10, - 11: migrateFrom11, } ) @@ -277,7 +270,7 @@ func newSqliteCache(filename, startupQueries string, cacheDuration time.Duration if err != nil { return nil, err } - if err := setupMessagesDB(db, startupQueries, cacheDuration); err != nil { + if err := setupDB(db, startupQueries, cacheDuration); err != nil { return nil, err } var queue *util.BatchingQueue[*message] @@ -391,7 +384,6 @@ func (c *messageCache) addMessages(ms []*message) error { attachmentDeleted, // Always zero sender, m.User, - m.ContentType, m.Encoding, published, ) @@ -664,7 +656,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) { func readMessage(rows *sql.Rows) (*message, error) { var timestamp, expires, attachmentSize, attachmentExpires int64 var priority int - var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, contentType, encoding string + var id, topic, msg, title, tagsStr, click, icon, actionsStr, attachmentName, attachmentType, attachmentURL, sender, user, encoding string err := rows.Scan( &id, ×tamp, @@ -684,7 +676,6 @@ func readMessage(rows *sql.Rows) (*message, error) { &attachmentURL, &sender, &user, - &contentType, &encoding, ) if err != nil { @@ -715,23 +706,22 @@ func readMessage(rows *sql.Rows) (*message, error) { } } return &message{ - ID: id, - Time: timestamp, - Expires: expires, - Event: messageEvent, - Topic: topic, - Message: msg, - Title: title, - Priority: priority, - Tags: tags, - Click: click, - Icon: icon, - Actions: actions, - Attachment: att, - Sender: senderIP, // Must parse assuming database must be correct - User: user, - ContentType: contentType, - Encoding: encoding, + ID: id, + Time: timestamp, + Expires: expires, + Event: messageEvent, + Topic: topic, + Message: msg, + Title: title, + Priority: priority, + Tags: tags, + Click: click, + Icon: icon, + Actions: actions, + Attachment: att, + Sender: senderIP, // Must parse assuming database must be correct + User: user, + Encoding: encoding, }, nil } @@ -759,7 +749,7 @@ func (c *messageCache) Close() error { return c.db.Close() } -func setupMessagesDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error { +func setupDB(db *sql.DB, startupQueries string, cacheDuration time.Duration) error { // Run startup queries if startupQueries != "" { if _, err := db.Exec(startupQueries); err != nil { @@ -939,7 +929,7 @@ func migrateFrom9(db *sql.DB, cacheDuration time.Duration) error { return tx.Commit() } -func migrateFrom10(db *sql.DB, _ time.Duration) error { +func migrateFrom10(db *sql.DB, cacheDuration time.Duration) error { log.Tag(tagMessageCache).Info("Migrating cache database schema: from 10 to 11") tx, err := db.Begin() if err != nil { @@ -954,19 +944,3 @@ func migrateFrom10(db *sql.DB, _ time.Duration) error { } return tx.Commit() } - -func migrateFrom11(db *sql.DB, _ time.Duration) error { - log.Tag(tagMessageCache).Info("Migrating cache database schema: from 11 to 12") - tx, err := db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - if _, err := tx.Exec(migrate11To12AlterMessagesTableQuery); err != nil { - return err - } - if _, err := tx.Exec(updateSchemaVersion, 12); err != nil { - return err - } - return tx.Commit() -} diff --git a/server/server.go b/server/server.go index 8610f443..ac54aa50 100644 --- a/server/server.go +++ b/server/server.go @@ -9,6 +9,13 @@ import ( "encoding/json" "errors" "fmt" + "github.com/emersion/go-smtp" + "github.com/gorilla/websocket" + "github.com/prometheus/client_golang/prometheus/promhttp" + "golang.org/x/sync/errgroup" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net" "net/http" @@ -25,14 +32,6 @@ import ( "sync" "time" "unicode/utf8" - - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" - "github.com/emersion/go-smtp" - "github.com/gorilla/websocket" - "github.com/prometheus/client_golang/prometheus/promhttp" - "golang.org/x/sync/errgroup" ) // Server is the main server, providing the UI and API for ntfy @@ -53,7 +52,6 @@ type Server struct { messagesHistory []int64 // Last n values of the messages counter, used to determine rate userManager *user.Manager // Might be nil! messageCache *messageCache // Database that stores the messages - webPush *webPushStore // Database that stores web push subscriptions fileCache *fileCache // File system based cache that stores attachments stripe stripeAPI // Stripe API, can be replaced with a mock priceCache *util.LookupCache[map[string]int64] // Stripe price ID -> price as cents (USD implied!) @@ -78,15 +76,11 @@ var ( publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`) webConfigPath = "/config.js" - webManifestPath = "/manifest.webmanifest" - webRootHTMLPath = "/app.html" - webServiceWorkerPath = "/sw.js" accountPath = "/account" matrixPushPath = "/_matrix/push/v1/notify" metricsPath = "/metrics" apiHealthPath = "/v1/health" apiStatsPath = "/v1/stats" - apiWebPushPath = "/v1/webpush" apiTiersPath = "/v1/tiers" apiUsersPath = "/v1/users" apiUsersAccessPath = "/v1/users/access" @@ -157,13 +151,6 @@ func New(conf *Config) (*Server, error) { if err != nil { return nil, err } - var webPush *webPushStore - if conf.WebPushPublicKey != "" { - webPush, err = newWebPushStore(conf.WebPushFile, conf.WebPushStartupQueries) - if err != nil { - return nil, err - } - } topics, err := messageCache.Topics() if err != nil { return nil, err @@ -203,7 +190,6 @@ func New(conf *Config) (*Server, error) { s := &Server{ config: conf, messageCache: messageCache, - webPush: webPush, fileCache: fileCache, firebaseClient: firebaseClient, smtpSender: mailer, @@ -356,9 +342,6 @@ func (s *Server) closeDatabases() { s.userManager.Close() } s.messageCache.Close() - if s.webPush != nil { - s.webPush.Close() - } } // handle is the main entry point for all HTTP requests @@ -433,8 +416,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleHealth(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == webConfigPath { return s.ensureWebEnabled(s.handleWebConfig)(w, r, v) - } else if r.Method == http.MethodGet && r.URL.Path == webManifestPath { - return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath { return s.ensureAdmin(s.handleUsersGet)(w, r, v) } else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath { @@ -489,10 +470,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v) } else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath { return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v) - } else if r.Method == http.MethodPost && apiWebPushPath == r.URL.Path { - return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushUpdate))(w, r, v) - } else if r.Method == http.MethodDelete && apiWebPushPath == r.URL.Path { - return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushDelete))(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiStatsPath { return s.handleStats(w, r, v) } else if r.Method == http.MethodGet && r.URL.Path == apiTiersPath { @@ -501,7 +478,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit return s.handleMatrixDiscovery(w) } else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil { return s.handleMetrics(w, r, v) - } else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) { + } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleStatic)(w, r, v) } else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) { return s.ensureWebEnabled(s.handleDocs)(w, r, v) @@ -575,9 +552,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi EnableCalls: s.config.TwilioAccount != "", EnableEmails: s.config.SMTPSenderFrom != "", EnableReservations: s.config.EnableReservations, - EnableWebPush: s.config.WebPushPublicKey != "", BillingContact: s.config.BillingContact, - WebPushPublicKey: s.config.WebPushPublicKey, DisallowedTopics: s.config.DisallowedTopics, } b, err := json.MarshalIndent(response, "", " ") @@ -589,25 +564,6 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi return err } -// handleWebManifest serves the web app manifest for the progressive web app (PWA) -func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *visitor) error { - response := &webManifestResponse{ - Name: "ntfy web", - Description: "ntfy lets you send push notifications via scripts from any computer or phone", - ShortName: "ntfy", - Scope: "/", - StartURL: s.config.WebRoot, - Display: "standalone", - BackgroundColor: "#ffffff", - ThemeColor: "#317f6f", - Icons: []*webManifestIcon{ - {SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"}, - {SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"}, - }, - } - return s.writeJSONWithContentType(w, response, "application/manifest+json") -} - // handleMetrics returns Prometheus metrics. This endpoint is only called if enable-metrics is set, // and listen-metrics-http is not set. func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request, _ *visitor) error { @@ -804,12 +760,9 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if s.config.TwilioAccount != "" && call != "" { go s.callPhone(v, r, m, call) } - if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream + if s.config.UpstreamBaseURL != "" { go s.forwardPollRequest(v, m) } - if s.config.WebPushPublicKey != "" { - go s.publishToWebPushEndpoints(v, m) - } } else { logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later") } @@ -1010,10 +963,6 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } - contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") - if markdown || strings.ToLower(contentType) == "text/markdown" { - m.ContentType = "text/markdown" - } unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! if unifiedpush { firebase = false @@ -1743,9 +1692,6 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error { if s.config.UpstreamBaseURL != "" { go s.forwardPollRequest(v, m) } - if s.config.WebPushPublicKey != "" { - go s.publishToWebPushEndpoints(v, m) - } if err := s.messageCache.MarkPublished(m); err != nil { return err } @@ -1789,9 +1735,6 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc { if m.Icon != "" { r.Header.Set("X-Icon", m.Icon) } - if m.Markdown { - r.Header.Set("X-Markdown", "yes") - } if len(m.Actions) > 0 { actionsStr, err := json.Marshal(m.Actions) if err != nil { @@ -1969,11 +1912,7 @@ func (s *Server) visitor(ip netip.Addr, user *user.User) *visitor { } func (s *Server) writeJSON(w http.ResponseWriter, v any) error { - return s.writeJSONWithContentType(w, v, "application/json") -} - -func (s *Server) writeJSONWithContentType(w http.ResponseWriter, v any, contentType string) error { - w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests if err := json.NewEncoder(w).Encode(v); err != nil { return err diff --git a/server/server.yml b/server/server.yml index b044a914..9c7972e9 100644 --- a/server/server.yml +++ b/server/server.yml @@ -144,27 +144,6 @@ # smtp-server-domain: # smtp-server-addr-prefix: -# Web Push support (background notifications for browsers) -# -# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users -# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push -# endpoint, which will then forward it to the browser. -# -# You must configure web-push-public/private key, web-push-file, and web-push-email-address below to enable Web Push. -# Run "ntfy webpush keys" to generate the keys. -# -# - web-push-public-key is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 -# - web-push-private-key is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890 -# - web-push-file is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db` -# - web-push-email-address is the admin email address send to the push provider, e.g. `sysadmin@example.com` -# - web-push-startup-queries is an optional list of queries to run on startup` -# -# web-push-public-key: -# web-push-private-key: -# web-push-file: -# web-push-email-address: -# web-push-startup-queries: - # If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header. # # - twilio-account is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586 @@ -342,10 +321,6 @@ # - "field -> level" to match any value, e.g. "time_taken_ms -> debug" # Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging. # -# Check your permissions: -# If you are running ntfy with systemd, make sure this log file is owned by the -# ntfy user and group by running: chown ntfy.ntfy . -# # Example (good for production): # log-level: info # log-format: json diff --git a/server/server_account.go b/server/server_account.go index 32b6153f..6e6a6864 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -2,9 +2,9 @@ package server import ( "encoding/json" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "net/http" "net/netip" "strings" @@ -170,11 +170,6 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v * if _, err := s.userManager.Authenticate(u.Name, req.Password); err != nil { return errHTTPBadRequestIncorrectPasswordConfirmation } - if s.webPush != nil && u.ID != "" { - if err := s.webPush.RemoveSubscriptionsByUserID(u.ID); err != nil { - logvr(v, r).Err(err).Warn("Error removing web push subscriptions for %s", u.Name) - } - } if u.Billing.StripeSubscriptionID != "" { logvr(v, r).Tag(tagStripe).Info("Canceling billing subscription for user %s", u.Name) if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil { diff --git a/server/server_account_test.go b/server/server_account_test.go index fd51ac27..119efb16 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -2,10 +2,10 @@ package server import ( "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net/netip" "path/filepath" diff --git a/server/server_admin.go b/server/server_admin.go index 5bfd1547..9380a5ff 100644 --- a/server/server_admin.go +++ b/server/server_admin.go @@ -1,7 +1,7 @@ package server import ( - "git.zio.sh/astra/ntfy/v2/user" + "heckel.io/ntfy/user" "net/http" ) diff --git a/server/server_admin_test.go b/server/server_admin_test.go index a2a6f432..1513ea40 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -1,9 +1,9 @@ package server import ( - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "sync/atomic" "testing" "time" diff --git a/server/server_firebase.go b/server/server_firebase.go index eb1db971..6318b98e 100644 --- a/server/server_firebase.go +++ b/server/server_firebase.go @@ -7,9 +7,9 @@ import ( firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "fmt" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "google.golang.org/api/option" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "strings" ) @@ -144,18 +144,17 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro } if allowForward { data = map[string]string{ - "id": m.ID, - "time": fmt.Sprintf("%d", m.Time), - "event": m.Event, - "topic": m.Topic, - "priority": fmt.Sprintf("%d", m.Priority), - "tags": strings.Join(m.Tags, ","), - "click": m.Click, - "icon": m.Icon, - "title": m.Title, - "message": m.Message, - "content_type": m.ContentType, - "encoding": m.Encoding, + "id": m.ID, + "time": fmt.Sprintf("%d", m.Time), + "event": m.Event, + "topic": m.Topic, + "priority": fmt.Sprintf("%d", m.Priority), + "tags": strings.Join(m.Tags, ","), + "click": m.Click, + "icon": m.Icon, + "title": m.Title, + "message": m.Message, + "encoding": m.Encoding, } if len(m.Actions) > 0 { actions, err := json.Marshal(m.Actions) diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index ca1147e3..f18abe13 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/user" + "heckel.io/ntfy/user" "net/netip" "strings" "sync" @@ -182,7 +182,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, - "content_type": "", "encoding": "", "attachment_name": "some file.jpg", "attachment_type": "image/jpeg", @@ -204,7 +203,6 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) { "title": "some title", "message": "this is a message", "actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`, - "content_type": "", "encoding": "", "attachment_name": "some file.jpg", "attachment_type": "image/jpeg", diff --git a/server/server_manager.go b/server/server_manager.go index 7a562a94..52e3621e 100644 --- a/server/server_manager.go +++ b/server/server_manager.go @@ -1,8 +1,8 @@ package server import ( - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "strings" ) @@ -15,7 +15,6 @@ func (s *Server) execManager() { s.pruneTokens() s.pruneAttachments() s.pruneMessages() - s.pruneAndNotifyWebPushSubscriptions() // Message count per topic var messagesCached int diff --git a/server/server_matrix.go b/server/server_matrix.go index bf43a13f..c25a1b59 100644 --- a/server/server_matrix.go +++ b/server/server_matrix.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" "io" "net/http" "strings" diff --git a/server/server_middleware.go b/server/server_middleware.go index 5d842b98..7aea45a3 100644 --- a/server/server_middleware.go +++ b/server/server_middleware.go @@ -3,7 +3,7 @@ package server import ( "net/http" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" ) type contextKey int @@ -58,15 +58,6 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc { } } -func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc { - return func(w http.ResponseWriter, r *http.Request, v *visitor) error { - if s.config.WebRoot == "" || s.config.WebPushPublicKey == "" { - return errHTTPNotFound - } - return next(w, r, v) - } -} - func (s *Server) ensureUserManager(next handleFunc) handleFunc { return func(w http.ResponseWriter, r *http.Request, v *visitor) error { if s.userManager == nil { diff --git a/server/server_payments.go b/server/server_payments.go index a4b51a11..1e98d059 100644 --- a/server/server_payments.go +++ b/server/server_payments.go @@ -4,9 +4,6 @@ import ( "bytes" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stripe/stripe-go/v74" portalsession "github.com/stripe/stripe-go/v74/billingportal/session" "github.com/stripe/stripe-go/v74/checkout/session" @@ -14,6 +11,9 @@ import ( "github.com/stripe/stripe-go/v74/price" "github.com/stripe/stripe-go/v74/subscription" "github.com/stripe/stripe-go/v74/webhook" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net/http" "net/netip" diff --git a/server/server_payments_test.go b/server/server_payments_test.go index 29a1b13d..ebd559e7 100644 --- a/server/server_payments_test.go +++ b/server/server_payments_test.go @@ -2,12 +2,12 @@ package server import ( "encoding/json" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stripe/stripe-go/v74" "golang.org/x/time/rate" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net/netip" "path/filepath" diff --git a/server/server_test.go b/server/server_test.go index 85fdc211..73df2762 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -6,8 +6,8 @@ import ( "encoding/base64" "encoding/json" "fmt" - "git.zio.sh/astra/ntfy/v2/user" "golang.org/x/crypto/bcrypt" + "heckel.io/ntfy/user" "io" "math/rand" "net/http" @@ -22,10 +22,9 @@ import ( "testing" "time" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" - "github.com/SherClockHolmes/webpush-go" "github.com/stretchr/testify/require" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) func TestMain(m *testing.M) { @@ -239,12 +238,6 @@ func TestServer_WebEnabled(t *testing.T) { rr = request(t, s, "GET", "/config.js", "", nil) require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/sw.js", "", nil) - require.Equal(t, 404, rr.Code) - - rr = request(t, s, "GET", "/app.html", "", nil) - require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/static/css/home.css", "", nil) require.Equal(t, 404, rr.Code) @@ -257,35 +250,6 @@ func TestServer_WebEnabled(t *testing.T) { rr = request(t, s2, "GET", "/config.js", "", nil) require.Equal(t, 200, rr.Code) - - rr = request(t, s2, "GET", "/sw.js", "", nil) - require.Equal(t, 200, rr.Code) - - rr = request(t, s2, "GET", "/app.html", "", nil) - require.Equal(t, 200, rr.Code) -} - -func TestServer_WebPushEnabled(t *testing.T) { - conf := newTestConfig(t) - conf.WebRoot = "" // Disable web app - s := newTestServer(t, conf) - - rr := request(t, s, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 404, rr.Code) - - conf2 := newTestConfig(t) - s2 := newTestServer(t, conf2) - - rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 404, rr.Code) - - conf3 := newTestConfigWithWebPush(t) - s3 := newTestServer(t, conf3) - - rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 200, rr.Code) - require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type")) - } func TestServer_PublishLargeMessage(t *testing.T) { @@ -329,27 +293,6 @@ func TestServer_PublishPriority(t *testing.T) { require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code) } -func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "Priority": "u=4", - "X-Priority": "5", - }) - require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) - - response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{ - "Priority": "u=9", - }) - require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) - - response = request(t, s, "POST", "/mytopic", "test", map[string]string{ - "p": "2", - "priority": "u=9, i", - }) - require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) -} - func TestServer_PublishGETOnlyOneTopic(t *testing.T) { // This tests a bug that allowed publishing topics with a comma in the name (no ticket) @@ -512,8 +455,6 @@ func TestServer_PublishAtAndPrune(t *testing.T) { messages := toMessages(t, response.Body.String()) require.Equal(t, 1, len(messages)) // Not affected by pruning require.Equal(t, "a message", messages[0].Message) - - time.Sleep(time.Second) // FIXME CI failing not sure why } func TestServer_PublishAndMultiPoll(t *testing.T) { @@ -1541,39 +1482,6 @@ func TestServer_PublishActions_AndPoll(t *testing.T) { require.Equal(t, "target_temp_f=65", m.Actions[1].Body) } -func TestServer_PublishMarkdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ - "Content-Type": "text/markdown", - }) - require.Equal(t, 200, response.Code) - - m := toMessage(t, response.Body.String()) - require.Equal(t, "**make this bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) -} - -func TestServer_PublishMarkdown_QueryParam(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil) - require.Equal(t, 200, response.Code) - - m := toMessage(t, response.Body.String()) - require.Equal(t, "**make this bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) -} - -func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ - "Content-Type": "not-markdown", - }) - require.Equal(t, 200, response.Code) - - m := toMessage(t, response.Body.String()) - require.Equal(t, "", m.ContentType) -} - func TestServer_PublishAsJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + @@ -1591,25 +1499,12 @@ func TestServer_PublishAsJSON(t *testing.T) { require.Equal(t, "google.pdf", m.Attachment.Name) require.Equal(t, "http://ntfy.sh", m.Click) require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) - require.Equal(t, "", m.ContentType) require.Equal(t, 4, m.Priority) require.True(t, m.Time > time.Now().Unix()+29*60) require.True(t, m.Time < time.Now().Unix()+31*60) } -func TestServer_PublishAsJSON_Markdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 200, response.Code) - - m := toMessage(t, response.Body.String()) - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "**This is bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) -} - func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) { // Publishing as JSON follows a different path. This ensures that rate // limiting works for this endpoint as well @@ -2664,29 +2559,6 @@ func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) { }) } -func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("UnifiedPush messages should not be forwarded") - })) - defer upstreamServer.Close() - - c := newTestConfigWithAuthFile(t) - c.BaseURL = "http://myserver.internal" - c.UpstreamBaseURL = upstreamServer.URL - s := newTestServer(t, c) - - // Send UP message, this should not forward to upstream server - response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.NotEmpty(t, m.ID) - require.Equal(t, "hi there", m.Message) - - // Forwarding is done asynchronously, so wait a bit. - // This ensures that the t.Fatal above is actually not triggered. - time.Sleep(500 * time.Millisecond) -} - func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" @@ -2696,33 +2568,19 @@ func newTestConfig(t *testing.T) *Config { return conf } -func configureAuth(t *testing.T, conf *Config) *Config { +func newTestConfigWithAuthFile(t *testing.T) *Config { + conf := newTestConfig(t) conf.AuthFile = filepath.Join(t.TempDir(), "user.db") conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot return conf } -func newTestConfigWithAuthFile(t *testing.T) *Config { - conf := newTestConfig(t) - conf = configureAuth(t, conf) - return conf -} - -func newTestConfigWithWebPush(t *testing.T) *Config { - conf := newTestConfig(t) - privateKey, publicKey, err := webpush.GenerateVAPIDKeys() - require.Nil(t, err) - conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db") - conf.WebPushEmailAddress = "testing@example.com" - conf.WebPushPrivateKey = privateKey - conf.WebPushPublicKey = publicKey - return conf -} - func newTestServer(t *testing.T, config *Config) *Server { server, err := New(config) - require.Nil(t, err) + if err != nil { + t.Fatal(err) + } return server } diff --git a/server/server_twilio.go b/server/server_twilio.go index 231436a3..093abe63 100644 --- a/server/server_twilio.go +++ b/server/server_twilio.go @@ -4,9 +4,9 @@ import ( "bytes" "encoding/xml" "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net/http" "net/url" diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go index d6877527..af694a77 100644 --- a/server/server_twilio_test.go +++ b/server/server_twilio_test.go @@ -1,9 +1,9 @@ package server import ( - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/user" + "heckel.io/ntfy/util" "io" "net/http" "net/http/httptest" diff --git a/server/server_webpush.go b/server/server_webpush.go deleted file mode 100644 index a0e33af5..00000000 --- a/server/server_webpush.go +++ /dev/null @@ -1,171 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - "github.com/SherClockHolmes/webpush-go" -) - -const ( - webPushTopicSubscribeLimit = 50 -) - -var ( - webPushAllowedEndpointsPatterns = []string{ - "https://*.google.com/", - "https://*.googleapis.com/", - "https://*.mozilla.com/", - "https://*.mozaws.net/", - "https://*.windows.com/", - "https://*.microsoft.com/", - "https://*.apple.com/", - } - webPushAllowedEndpointsRegex *regexp.Regexp -) - -func init() { - for i, pattern := range webPushAllowedEndpointsPatterns { - webPushAllowedEndpointsPatterns[i] = strings.ReplaceAll(strings.ReplaceAll(pattern, ".", "\\."), "*", ".+") - } - allPatterns := fmt.Sprintf("^(%s)", strings.Join(webPushAllowedEndpointsPatterns, "|")) - webPushAllowedEndpointsRegex = regexp.MustCompile(allPatterns) -} - -func (s *Server) handleWebPushUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) - if err != nil || req.Endpoint == "" || req.P256dh == "" || req.Auth == "" { - return errHTTPBadRequestWebPushSubscriptionInvalid - } else if !webPushAllowedEndpointsRegex.MatchString(req.Endpoint) { - return errHTTPBadRequestWebPushEndpointUnknown - } else if len(req.Topics) > webPushTopicSubscribeLimit { - return errHTTPBadRequestWebPushTopicCountTooHigh - } - topics, err := s.topicsFromIDs(req.Topics...) - if err != nil { - return err - } - if s.userManager != nil { - u := v.User() - for _, t := range topics { - if err := s.userManager.Authorize(u, t.ID, user.PermissionRead); err != nil { - logvr(v, r).With(t).Err(err).Debug("Access to topic %s not authorized", t.ID) - return errHTTPForbidden.With(t) - } - } - } - if err := s.webPush.UpsertSubscription(req.Endpoint, req.Auth, req.P256dh, v.MaybeUserID(), v.IP(), req.Topics); err != nil { - return err - } - return s.writeJSON(w, newSuccessResponse()) -} - -func (s *Server) handleWebPushDelete(w http.ResponseWriter, r *http.Request, _ *visitor) error { - req, err := readJSONWithLimit[apiWebPushUpdateSubscriptionRequest](r.Body, jsonBodyBytesLimit, false) - if err != nil || req.Endpoint == "" { - return errHTTPBadRequestWebPushSubscriptionInvalid - } - if err := s.webPush.RemoveSubscriptionsByEndpoint(req.Endpoint); err != nil { - return err - } - return s.writeJSON(w, newSuccessResponse()) -} - -func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) { - subscriptions, err := s.webPush.SubscriptionsForTopic(m.Topic) - if err != nil { - logvm(v, m).Err(err).With(v, m).Warn("Unable to publish web push messages") - return - } - log.Tag(tagWebPush).With(v, m).Debug("Publishing web push message to %d subscribers", len(subscriptions)) - payload, err := json.Marshal(newWebPushPayload(fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic), m)) - if err != nil { - log.Tag(tagWebPush).Err(err).With(v, m).Warn("Unable to marshal expiring payload") - return - } - for _, subscription := range subscriptions { - if err := s.sendWebPushNotification(subscription, payload, v, m); err != nil { - log.Tag(tagWebPush).Err(err).With(v, m, subscription).Warn("Unable to publish web push message") - } - } -} - -func (s *Server) pruneAndNotifyWebPushSubscriptions() { - if s.config.WebPushPublicKey == "" { - return - } - go func() { - if err := s.pruneAndNotifyWebPushSubscriptionsInternal(); err != nil { - log.Tag(tagWebPush).Err(err).Warn("Unable to prune or notify web push subscriptions") - } - }() -} - -func (s *Server) pruneAndNotifyWebPushSubscriptionsInternal() error { - // Expire old subscriptions - if err := s.webPush.RemoveExpiredSubscriptions(s.config.WebPushExpiryDuration); err != nil { - return err - } - // Notify subscriptions that will expire soon - subscriptions, err := s.webPush.SubscriptionsExpiring(s.config.WebPushExpiryWarningDuration) - if err != nil { - return err - } else if len(subscriptions) == 0 { - return nil - } - payload, err := json.Marshal(newWebPushSubscriptionExpiringPayload()) - if err != nil { - return err - } - warningSent := make([]*webPushSubscription, 0) - for _, subscription := range subscriptions { - if err := s.sendWebPushNotification(subscription, payload); err != nil { - log.Tag(tagWebPush).Err(err).With(subscription).Warn("Unable to publish expiry imminent warning") - continue - } - warningSent = append(warningSent, subscription) - } - if err := s.webPush.MarkExpiryWarningSent(warningSent); err != nil { - return err - } - log.Tag(tagWebPush).Debug("Expired old subscriptions and published %d expiry imminent warnings", len(subscriptions)) - return nil -} - -func (s *Server) sendWebPushNotification(sub *webPushSubscription, message []byte, contexters ...log.Contexter) error { - log.Tag(tagWebPush).With(sub).With(contexters...).Debug("Sending web push message") - payload := &webpush.Subscription{ - Endpoint: sub.Endpoint, - Keys: webpush.Keys{ - Auth: sub.Auth, - P256dh: sub.P256dh, - }, - } - resp, err := webpush.SendNotification(message, payload, &webpush.Options{ - Subscriber: s.config.WebPushEmailAddress, - VAPIDPublicKey: s.config.WebPushPublicKey, - VAPIDPrivateKey: s.config.WebPushPrivateKey, - Urgency: webpush.UrgencyHigh, // iOS requires this to ensure delivery - TTL: int(s.config.CacheDuration.Seconds()), - }) - if err != nil { - log.Tag(tagWebPush).With(sub).With(contexters...).Err(err).Debug("Unable to publish web push message, removing endpoint") - if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil { - return err - } - return err - } - if (resp.StatusCode < 200 || resp.StatusCode > 299) && resp.StatusCode != 429 { - log.Tag(tagWebPush).With(sub).With(contexters...).Field("response_code", resp.StatusCode).Debug("Unable to publish web push message, unexpected response") - if err := s.webPush.RemoveSubscriptionsByEndpoint(sub.Endpoint); err != nil { - return err - } - return errHTTPInternalErrorWebPushUnableToPublish.With(sub).With(contexters...) - } - return nil -} diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go deleted file mode 100644 index 16e02cc8..00000000 --- a/server/server_webpush_test.go +++ /dev/null @@ -1,256 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "git.zio.sh/astra/ntfy/v2/user" - "git.zio.sh/astra/ntfy/v2/util" - "github.com/stretchr/testify/require" - "io" - "net/http" - "net/http/httptest" - "net/netip" - "strings" - "sync/atomic" - "testing" - "time" -) - -const ( - testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF" -) - -func TestServer_WebPush_Disabled(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 404, response.Code) -} - -func TestServer_WebPush_TopicAdd(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - - require.Len(t, subs, 1) - require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) - require.Equal(t, subs[0].P256dh, "p256dh-key") - require.Equal(t, subs[0].Auth, "auth-key") - require.Equal(t, subs[0].UserID, "") -} - -func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil) - require.Equal(t, 400, response.Code) - require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String()) -} - -func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - topicList := make([]string, 51) - for i := range topicList { - topicList[i] = util.RandomString(5) - } - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil) - require.Equal(t, 400, response.Code) - require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String()) -} - -func TestServer_WebPush_TopicUnsubscribe(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - addSubscription(t, s, testWebPushEndpoint, "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - requireSubscriptionCount(t, s, "test-topic", 0) -} - -func TestServer_WebPush_Delete(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - addSubscription(t, s, testWebPushEndpoint, "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) - - response := request(t, s, "DELETE", "/v1/webpush", fmt.Sprintf(`{"endpoint":"%s"}`, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - requireSubscriptionCount(t, s, "test-topic", 0) -} - -func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - config.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, config) - - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) - require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - require.Len(t, subs, 1) - require.True(t, strings.HasPrefix(subs[0].UserID, "u_")) -} - -func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - config.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, config) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 403, response.Code) - - requireSubscriptionCount(t, s, "test-topic", 0) -} - -func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - s := newTestServer(t, config) - - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) - require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) - - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - requireSubscriptionCount(t, s, "test-topic", 1) - - request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - // should've been deleted with the account - requireSubscriptionCount(t, s, "test-topic", 0) -} - -func TestServer_WebPush_Publish(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - var received atomic.Bool - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - require.Equal(t, "/push-receive", r.URL.Path) - require.Equal(t, "high", r.Header.Get("Urgency")) - require.Equal(t, "", r.Header.Get("Topic")) - received.Store(true) - })) - defer pushService.Close() - - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") - request(t, s, "POST", "/test-topic", "web push test", nil) - - waitFor(t, func() bool { - return received.Load() - }) -} - -func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - var received atomic.Bool - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - w.WriteHeader(http.StatusGone) - received.Store(true) - })) - defer pushService.Close() - - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc") - requireSubscriptionCount(t, s, "test-topic", 1) - requireSubscriptionCount(t, s, "test-topic-abc", 1) - - request(t, s, "POST", "/test-topic", "web push test", nil) - - waitFor(t, func() bool { - return received.Load() - }) - - // Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint - - requireSubscriptionCount(t, s, "test-topic", 0) - requireSubscriptionCount(t, s, "test-topic-abc", 0) -} - -func TestServer_WebPush_Expiry(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) - - var received atomic.Bool - - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - w.WriteHeader(200) - w.Write([]byte(``)) - received.Store(true) - })) - defer pushService.Close() - - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) - - _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix()) - require.Nil(t, err) - - s.pruneAndNotifyWebPushSubscriptions() - requireSubscriptionCount(t, s, "test-topic", 1) - - waitFor(t, func() bool { - return received.Load() - }) - - _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix()) - require.Nil(t, err) - - s.pruneAndNotifyWebPushSubscriptions() - waitFor(t, func() bool { - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - return len(subs) == 0 - }) -} - -func payloadForTopics(t *testing.T, topics []string, endpoint string) string { - topicsJSON, err := json.Marshal(topics) - require.Nil(t, err) - - return fmt.Sprintf(`{ - "topics": %s, - "endpoint": "%s", - "p256dh": "p256dh-key", - "auth": "auth-key" - }`, topicsJSON, endpoint) -} - -func addSubscription(t *testing.T, s *Server, endpoint string, topics ...string) { - require.Nil(t, s.webPush.UpsertSubscription(endpoint, "kSC3T8aN1JCQxxPdrFLrZg", "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE", "u_123", netip.MustParseAddr("1.2.3.4"), topics)) // Test auth and p256dh -} - -func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) { - subs, err := s.webPush.SubscriptionsForTopic(topic) - require.Nil(t, err) - require.Len(t, subs, expectedLength) -} diff --git a/server/smtp_sender.go b/server/smtp_sender.go index 4c0d263e..9093687e 100644 --- a/server/smtp_sender.go +++ b/server/smtp_sender.go @@ -11,8 +11,8 @@ import ( "sync" "time" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) type mailer interface { diff --git a/server/topic.go b/server/topic.go index 3daac9a7..5dfafbe3 100644 --- a/server/topic.go +++ b/server/topic.go @@ -5,8 +5,8 @@ import ( "sync" "time" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" ) const ( diff --git a/server/types.go b/server/types.go index 5c423216..9e4ff558 100644 --- a/server/types.go +++ b/server/types.go @@ -1,14 +1,13 @@ package server import ( + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" "net/http" "net/netip" "time" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" - - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" ) // List of possible events @@ -25,24 +24,23 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - Time int64 `json:"time"` // Unix time in seconds - Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) - Event string `json:"event"` // One of the above - Topic string `json:"topic"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` - Priority int `json:"priority,omitempty"` - Tags []string `json:"tags,omitempty"` - Click string `json:"click,omitempty"` - Icon string `json:"icon,omitempty"` - Actions []*action `json:"actions,omitempty"` - Attachment *attachment `json:"attachment,omitempty"` - PollID string `json:"poll_id,omitempty"` - ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown - Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes - Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting - User string `json:"-"` // UserID of the uploader, used to associated attachments + ID string `json:"id"` // Random message ID + Time int64 `json:"time"` // Unix time in seconds + Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive) + Event string `json:"event"` // One of the above + Topic string `json:"topic"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` + Priority int `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + Click string `json:"click,omitempty"` + Icon string `json:"icon,omitempty"` + Actions []*action `json:"actions,omitempty"` + Attachment *attachment `json:"attachment,omitempty"` + PollID string `json:"poll_id,omitempty"` + Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes + Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting + User string `json:"-"` // Username of the uploader, used to associated attachments } func (m *message) Context() log.Context { @@ -101,7 +99,6 @@ type publishMessage struct { Icon string `json:"icon"` Actions []action `json:"actions"` Attach string `json:"attach"` - Markdown bool `json:"markdown"` Filename string `json:"filename"` Email string `json:"email"` Call string `json:"call"` @@ -400,9 +397,7 @@ type apiConfigResponse struct { EnableCalls bool `json:"enable_calls"` EnableEmails bool `json:"enable_emails"` EnableReservations bool `json:"enable_reservations"` - EnableWebPush bool `json:"enable_web_push"` BillingContact string `json:"billing_contact"` - WebPushPublicKey string `json:"web_push_public_key"` DisallowedTopics []string `json:"disallowed_topics"` } @@ -467,75 +462,3 @@ type apiStripeSubscriptionDeletedEvent struct { ID string `json:"id"` Customer string `json:"customer"` } - -type apiWebPushUpdateSubscriptionRequest struct { - Endpoint string `json:"endpoint"` - Auth string `json:"auth"` - P256dh string `json:"p256dh"` - Topics []string `json:"topics"` -} - -// List of possible Web Push events (see sw.js) -const ( - webPushMessageEvent = "message" - webPushExpiringEvent = "subscription_expiring" -) - -type webPushPayload struct { - Event string `json:"event"` - SubscriptionID string `json:"subscription_id"` - Message *message `json:"message"` -} - -func newWebPushPayload(subscriptionID string, message *message) *webPushPayload { - return &webPushPayload{ - Event: webPushMessageEvent, - SubscriptionID: subscriptionID, - Message: message, - } -} - -type webPushControlMessagePayload struct { - Event string `json:"event"` -} - -func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload { - return &webPushControlMessagePayload{ - Event: webPushExpiringEvent, - } -} - -type webPushSubscription struct { - ID string - Endpoint string - Auth string - P256dh string - UserID string -} - -func (w *webPushSubscription) Context() log.Context { - return map[string]any{ - "web_push_subscription_id": w.ID, - "web_push_subscription_user_id": w.UserID, - "web_push_subscription_endpoint": w.Endpoint, - } -} - -// https://developer.mozilla.org/en-US/docs/Web/Manifest -type webManifestResponse struct { - Name string `json:"name"` - Description string `json:"description"` - ShortName string `json:"short_name"` - Scope string `json:"scope"` - StartURL string `json:"start_url"` - Display string `json:"display"` - BackgroundColor string `json:"background_color"` - ThemeColor string `json:"theme_color"` - Icons []*webManifestIcon `json:"icons"` -} - -type webManifestIcon struct { - SRC string `json:"src"` - Sizes string `json:"sizes"` - Type string `json:"type"` -} diff --git a/server/util.go b/server/util.go index dea13ed5..03eb8661 100644 --- a/server/util.go +++ b/server/util.go @@ -3,19 +3,15 @@ package server import ( "context" "fmt" - "git.zio.sh/astra/ntfy/v2/util" + "heckel.io/ntfy/util" "io" "mime" "net/http" "net/netip" - "regexp" "strings" ) -var ( - mimeDecoder mime.WordDecoder - priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`) -) +var mimeDecoder mime.WordDecoder func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { value := strings.ToLower(readParam(r, names...)) @@ -54,9 +50,9 @@ func readParam(r *http.Request, names ...string) string { func readHeaderParam(r *http.Request, names ...string) string { for _, name := range names { - value := strings.TrimSpace(maybeDecodeHeader(name, r.Header.Get(name))) + value := maybeDecodeHeader(r.Header.Get(name)) if value != "" { - return value + return strings.TrimSpace(value) } } return "" @@ -130,26 +126,10 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) { return t, nil } -// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=", -// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader -// to ignore new HTTP "Priority" header. -func maybeDecodeHeader(name, value string) string { - decoded, err := mimeDecoder.DecodeHeader(value) +func maybeDecodeHeader(header string) string { + decoded, err := mimeDecoder.DecodeHeader(header) if err != nil { - return maybeIgnoreSpecialHeader(name, value) + return header } - return maybeIgnoreSpecialHeader(name, decoded) -} - -// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority) -// -// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy), -// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored. -// Returning an empty string will allow the rest of the logic to continue searching for another header (x-priority, prio, p), -// or in the Query parameters. -func maybeIgnoreSpecialHeader(name, value string) string { - if strings.ToLower(name) == "priority" && priorityHeaderIgnoreRegex.MatchString(strings.TrimSpace(value)) { - return "" - } - return value + return decoded } diff --git a/server/util_test.go b/server/util_test.go index 6555a81b..3d062b4d 100644 --- a/server/util_test.go +++ b/server/util_test.go @@ -2,9 +2,9 @@ package server import ( "bytes" - "crypto/rand" "fmt" "github.com/stretchr/testify/require" + "math/rand" "net/http" "strings" "testing" @@ -75,16 +75,3 @@ Accept: */* (peeked bytes not UTF-8, peek limit of 4096 bytes reached, hex: ` + fmt.Sprintf("%x", body[:4096]) + ` ...)` require.Equal(t, expected, renderHTTPRequest(r)) } - -func TestMaybeIgnoreSpecialHeader(t *testing.T) { - require.Empty(t, maybeIgnoreSpecialHeader("priority", "u=1")) - require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1")) - require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1, i")) -} - -func TestMaybeDecodeHeaders(t *testing.T) { - r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil) - r.Header.Set("Priority", "u=1") // Cloudflare priority header - r.Header.Set("X-Priority", "5") // ntfy priority header - require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p")) -} diff --git a/server/visitor.go b/server/visitor.go index d1ec1226..e4c06f66 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -2,14 +2,14 @@ package server import ( "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/user" + "heckel.io/ntfy/log" + "heckel.io/ntfy/user" "net/netip" "sync" "time" - "git.zio.sh/astra/ntfy/v2/util" "golang.org/x/time/rate" + "heckel.io/ntfy/util" ) const ( diff --git a/server/webpush_store.go b/server/webpush_store.go deleted file mode 100644 index 3781f250..00000000 --- a/server/webpush_store.go +++ /dev/null @@ -1,280 +0,0 @@ -package server - -import ( - "database/sql" - "errors" - "git.zio.sh/astra/ntfy/v2/util" - "net/netip" - "time" - - _ "github.com/mattn/go-sqlite3" // SQLite driver -) - -const ( - subscriptionIDPrefix = "wps_" - subscriptionIDLength = 10 - subscriptionEndpointLimitPerSubscriberIP = 10 -) - -var ( - errWebPushNoRows = errors.New("no rows found") - errWebPushTooManySubscriptions = errors.New("too many subscriptions") - errWebPushUserIDCannotBeEmpty = errors.New("user ID cannot be empty") -) - -const ( - createWebPushSubscriptionsTableQuery = ` - BEGIN; - CREATE TABLE IF NOT EXISTS subscription ( - id TEXT PRIMARY KEY, - endpoint TEXT NOT NULL, - key_auth TEXT NOT NULL, - key_p256dh TEXT NOT NULL, - user_id TEXT NOT NULL, - subscriber_ip TEXT NOT NULL, - updated_at INT NOT NULL, - warned_at INT NOT NULL DEFAULT 0 - ); - CREATE UNIQUE INDEX IF NOT EXISTS idx_endpoint ON subscription (endpoint); - CREATE INDEX IF NOT EXISTS idx_subscriber_ip ON subscription (subscriber_ip); - CREATE TABLE IF NOT EXISTS subscription_topic ( - subscription_id TEXT NOT NULL, - topic TEXT NOT NULL, - PRIMARY KEY (subscription_id, topic), - FOREIGN KEY (subscription_id) REFERENCES subscription (id) ON DELETE CASCADE - ); - CREATE INDEX IF NOT EXISTS idx_topic ON subscription_topic (topic); - CREATE TABLE IF NOT EXISTS schemaVersion ( - id INT PRIMARY KEY, - version INT NOT NULL - ); - COMMIT; - ` - builtinStartupQueries = ` - PRAGMA foreign_keys = ON; - ` - - selectWebPushSubscriptionIDByEndpoint = `SELECT id FROM subscription WHERE endpoint = ?` - selectWebPushSubscriptionCountBySubscriberIP = `SELECT COUNT(*) FROM subscription WHERE subscriber_ip = ?` - selectWebPushSubscriptionsForTopicQuery = ` - SELECT id, endpoint, key_auth, key_p256dh, user_id - FROM subscription_topic st - JOIN subscription s ON s.id = st.subscription_id - WHERE st.topic = ? - ORDER BY endpoint - ` - selectWebPushSubscriptionsExpiringSoonQuery = ` - SELECT id, endpoint, key_auth, key_p256dh, user_id - FROM subscription - WHERE warned_at = 0 AND updated_at <= ? - ` - insertWebPushSubscriptionQuery = ` - INSERT INTO subscription (id, endpoint, key_auth, key_p256dh, user_id, subscriber_ip, updated_at, warned_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (endpoint) - DO UPDATE SET key_auth = excluded.key_auth, key_p256dh = excluded.key_p256dh, user_id = excluded.user_id, subscriber_ip = excluded.subscriber_ip, updated_at = excluded.updated_at, warned_at = excluded.warned_at - ` - updateWebPushSubscriptionWarningSentQuery = `UPDATE subscription SET warned_at = ? WHERE id = ?` - deleteWebPushSubscriptionByEndpointQuery = `DELETE FROM subscription WHERE endpoint = ?` - deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?` - deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan! - - insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)` - deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?` -) - -// Schema management queries -const ( - currentWebPushSchemaVersion = 1 - insertWebPushSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` - selectWebPushSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` -) - -type webPushStore struct { - db *sql.DB -} - -func newWebPushStore(filename, startupQueries string) (*webPushStore, error) { - db, err := sql.Open("sqlite3", filename) - if err != nil { - return nil, err - } - if err := setupWebPushDB(db); err != nil { - return nil, err - } - if err := runWebPushStartupQueries(db, startupQueries); err != nil { - return nil, err - } - return &webPushStore{ - db: db, - }, nil -} - -func setupWebPushDB(db *sql.DB) error { - // If 'schemaVersion' table does not exist, this must be a new database - rows, err := db.Query(selectWebPushSchemaVersionQuery) - if err != nil { - return setupNewWebPushDB(db) - } - return rows.Close() -} - -func setupNewWebPushDB(db *sql.DB) error { - if _, err := db.Exec(createWebPushSubscriptionsTableQuery); err != nil { - return err - } - if _, err := db.Exec(insertWebPushSchemaVersion, currentWebPushSchemaVersion); err != nil { - return err - } - return nil -} - -func runWebPushStartupQueries(db *sql.DB, startupQueries string) error { - if _, err := db.Exec(startupQueries); err != nil { - return err - } - if _, err := db.Exec(builtinStartupQueries); err != nil { - return err - } - return nil -} - -// UpsertSubscription adds or updates Web Push subscriptions for the given topics and user ID. It always first deletes all -// existing entries for a given endpoint. -func (c *webPushStore) UpsertSubscription(endpoint string, auth, p256dh, userID string, subscriberIP netip.Addr, topics []string) error { - tx, err := c.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - // Read number of subscriptions for subscriber IP address - rowsCount, err := tx.Query(selectWebPushSubscriptionCountBySubscriberIP, subscriberIP.String()) - if err != nil { - return err - } - defer rowsCount.Close() - var subscriptionCount int - if !rowsCount.Next() { - return errWebPushNoRows - } - if err := rowsCount.Scan(&subscriptionCount); err != nil { - return err - } - if err := rowsCount.Close(); err != nil { - return err - } - // Read existing subscription ID for endpoint (or create new ID) - rows, err := tx.Query(selectWebPushSubscriptionIDByEndpoint, endpoint) - if err != nil { - return err - } - defer rows.Close() - var subscriptionID string - if rows.Next() { - if err := rows.Scan(&subscriptionID); err != nil { - return err - } - } else { - if subscriptionCount >= subscriptionEndpointLimitPerSubscriberIP { - return errWebPushTooManySubscriptions - } - subscriptionID = util.RandomStringPrefix(subscriptionIDPrefix, subscriptionIDLength) - } - if err := rows.Close(); err != nil { - return err - } - // Insert or update subscription - updatedAt, warnedAt := time.Now().Unix(), 0 - if _, err = tx.Exec(insertWebPushSubscriptionQuery, subscriptionID, endpoint, auth, p256dh, userID, subscriberIP.String(), updatedAt, warnedAt); err != nil { - return err - } - // Replace all subscription topics - if _, err := tx.Exec(deleteWebPushSubscriptionTopicAllQuery, subscriptionID); err != nil { - return err - } - for _, topic := range topics { - if _, err = tx.Exec(insertWebPushSubscriptionTopicQuery, subscriptionID, topic); err != nil { - return err - } - } - return tx.Commit() -} - -// SubscriptionsForTopic returns all subscriptions for the given topic -func (c *webPushStore) SubscriptionsForTopic(topic string) ([]*webPushSubscription, error) { - rows, err := c.db.Query(selectWebPushSubscriptionsForTopicQuery, topic) - if err != nil { - return nil, err - } - defer rows.Close() - return c.subscriptionsFromRows(rows) -} - -// SubscriptionsExpiring returns all subscriptions that have not been updated for a given time period -func (c *webPushStore) SubscriptionsExpiring(warnAfter time.Duration) ([]*webPushSubscription, error) { - rows, err := c.db.Query(selectWebPushSubscriptionsExpiringSoonQuery, time.Now().Add(-warnAfter).Unix()) - if err != nil { - return nil, err - } - defer rows.Close() - return c.subscriptionsFromRows(rows) -} - -// MarkExpiryWarningSent marks the given subscriptions as having received a warning about expiring soon -func (c *webPushStore) MarkExpiryWarningSent(subscriptions []*webPushSubscription) error { - tx, err := c.db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - for _, subscription := range subscriptions { - if _, err := tx.Exec(updateWebPushSubscriptionWarningSentQuery, time.Now().Unix(), subscription.ID); err != nil { - return err - } - } - return tx.Commit() -} - -func (c *webPushStore) subscriptionsFromRows(rows *sql.Rows) ([]*webPushSubscription, error) { - subscriptions := make([]*webPushSubscription, 0) - for rows.Next() { - var id, endpoint, auth, p256dh, userID string - if err := rows.Scan(&id, &endpoint, &auth, &p256dh, &userID); err != nil { - return nil, err - } - subscriptions = append(subscriptions, &webPushSubscription{ - ID: id, - Endpoint: endpoint, - Auth: auth, - P256dh: p256dh, - UserID: userID, - }) - } - return subscriptions, nil -} - -// RemoveSubscriptionsByEndpoint removes the subscription for the given endpoint -func (c *webPushStore) RemoveSubscriptionsByEndpoint(endpoint string) error { - _, err := c.db.Exec(deleteWebPushSubscriptionByEndpointQuery, endpoint) - return err -} - -// RemoveSubscriptionsByUserID removes all subscriptions for the given user ID -func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error { - if userID == "" { - return errWebPushUserIDCannotBeEmpty - } - _, err := c.db.Exec(deleteWebPushSubscriptionByUserIDQuery, userID) - return err -} - -// RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period -func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error { - _, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix()) - return err -} - -// Close closes the underlying database connection -func (c *webPushStore) Close() error { - return c.db.Close() -} diff --git a/server/webpush_store_test.go b/server/webpush_store_test.go deleted file mode 100644 index ab5bc424..00000000 --- a/server/webpush_store_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package server - -import ( - "fmt" - "github.com/stretchr/testify/require" - "net/netip" - "path/filepath" - "testing" - "time" -) - -func TestWebPushStore_UpsertSubscription_SubscriptionsForTopic(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - subs, err := webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) - require.Equal(t, subs[0].P256dh, "p256dh-key") - require.Equal(t, subs[0].Auth, "auth-key") - require.Equal(t, subs[0].UserID, "u_1234") - - subs2, err := webPush.SubscriptionsForTopic("mytopic") - require.Nil(t, err) - require.Len(t, subs2, 1) - require.Equal(t, subs[0].Endpoint, subs2[0].Endpoint) -} - -func TestWebPushStore_UpsertSubscription_SubscriberIPLimitReached(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert 10 subscriptions with the same IP address - for i := 0; i < 10; i++ { - endpoint := fmt.Sprintf(testWebPushEndpoint+"%d", i) - require.Nil(t, webPush.UpsertSubscription(endpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - } - - // Another one for the same endpoint should be fine - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different endpoint it should fail - require.Equal(t, errWebPushTooManySubscriptions, webPush.UpsertSubscription(testWebPushEndpoint+"11", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"test-topic", "mytopic"})) - - // But with a different IP address it should be fine again - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"99", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("9.9.9.9"), []string{"test-topic", "mytopic"})) -} - -func TestWebPushStore_UpsertSubscription_UpdateTopics(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics, and another with one topic - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"1", "auth-key", "p256dh-key", "", netip.MustParseAddr("9.9.9.9"), []string{"topic1"})) - - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - require.Equal(t, testWebPushEndpoint+"1", subs[1].Endpoint) - - subs, err = webPush.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - // Update the first subscription to have only one topic - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint+"0", "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1"})) - - subs, err = webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 2) - require.Equal(t, testWebPushEndpoint+"0", subs[0].Endpoint) - - subs, err = webPush.SubscriptionsForTopic("topic2") - require.Nil(t, err) - require.Len(t, subs, 0) -} - -func TestWebPushStore_RemoveSubscriptionsByEndpoint(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, webPush.RemoveSubscriptionsByEndpoint(testWebPushEndpoint)) - subs, err = webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) -} - -func TestWebPushStore_RemoveSubscriptionsByUserID(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // And remove it again - require.Nil(t, webPush.RemoveSubscriptionsByUserID("u_1234")) - subs, err = webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) -} - -func TestWebPushStore_RemoveSubscriptionsByUserID_Empty(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - require.Equal(t, errWebPushUserIDCannotBeEmpty, webPush.RemoveSubscriptionsByUserID("")) -} - -func TestWebPushStore_MarkExpiryWarningSent(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Mark them as warning sent - require.Nil(t, webPush.MarkExpiryWarningSent(subs)) - - rows, err := webPush.db.Query("SELECT endpoint FROM subscription WHERE warned_at > 0") - require.Nil(t, err) - defer rows.Close() - var endpoint string - require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&endpoint)) - require.Nil(t, err) - require.Equal(t, testWebPushEndpoint, endpoint) - require.False(t, rows.Next()) -} - -func TestWebPushStore_SubscriptionsExpiring(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as soon-to-expire - _, err = webPush.db.Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-8*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Should not be cleaned up yet - require.Nil(t, webPush.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // Run expiration - subs, err = webPush.SubscriptionsExpiring(7 * 24 * time.Hour) - require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, testWebPushEndpoint, subs[0].Endpoint) -} - -func TestWebPushStore_RemoveExpiredSubscriptions(t *testing.T) { - webPush := newTestWebPushStore(t) - defer webPush.Close() - - // Insert subscription with two topics - require.Nil(t, webPush.UpsertSubscription(testWebPushEndpoint, "auth-key", "p256dh-key", "u_1234", netip.MustParseAddr("1.2.3.4"), []string{"topic1", "topic2"})) - subs, err := webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 1) - - // Fake-mark them as expired - _, err = webPush.db.Exec("UPDATE subscription SET updated_at = ? WHERE endpoint = ?", time.Now().Add(-10*24*time.Hour).Unix(), testWebPushEndpoint) - require.Nil(t, err) - - // Run expiration - require.Nil(t, webPush.RemoveExpiredSubscriptions(9*24*time.Hour)) - - // List again, should be 0 - subs, err = webPush.SubscriptionsForTopic("topic1") - require.Nil(t, err) - require.Len(t, subs, 0) -} - -func newTestWebPushStore(t *testing.T) *webPushStore { - webPush, err := newWebPushStore(filepath.Join(t.TempDir(), "webpush.db"), "") - require.Nil(t, err) - return webPush -} diff --git a/test/server.go b/test/server.go index cabff94f..0b9200a6 100644 --- a/test/server.go +++ b/test/server.go @@ -2,7 +2,7 @@ package test import ( "fmt" - "git.zio.sh/astra/ntfy/v2/server" + "heckel.io/ntfy/server" "math/rand" "net/http" "path/filepath" diff --git a/user/manager.go b/user/manager.go index 0ff211cf..00407ab3 100644 --- a/user/manager.go +++ b/user/manager.go @@ -6,11 +6,11 @@ import ( "encoding/json" "errors" "fmt" - "git.zio.sh/astra/ntfy/v2/log" - "git.zio.sh/astra/ntfy/v2/util" "github.com/mattn/go-sqlite3" "github.com/stripe/stripe-go/v74" "golang.org/x/crypto/bcrypt" + "heckel.io/ntfy/log" + "heckel.io/ntfy/util" "net/netip" "strings" "sync" @@ -126,7 +126,6 @@ const ( ON CONFLICT (id) DO NOTHING; COMMIT; ` - builtinStartupQueries = ` PRAGMA foreign_keys = ON; ` @@ -160,7 +159,7 @@ const ( SELECT read, write FROM user_access a JOIN user u ON u.id = a.user_id - WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic ESCAPE '\' + WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic ORDER BY u.user DESC ` @@ -235,7 +234,7 @@ const ( selectOtherAccessCountQuery = ` SELECT COUNT(*) FROM user_access - WHERE (topic = ? OR ? LIKE topic ESCAPE '\') + WHERE (topic = ? OR ? LIKE topic) AND (owner_user_id IS NULL OR owner_user_id != (SELECT id FROM user WHERE user = ?)) ` deleteAllAccessQuery = `DELETE FROM user_access` @@ -262,8 +261,7 @@ const ( deleteExpiredTokensQuery = `DELETE FROM user_token WHERE expires > 0 AND expires < ?` deleteExcessTokensQuery = ` DELETE FROM user_token - WHERE user_id = ? - AND (user_id, token) NOT IN ( + WHERE (user_id, token) NOT IN ( SELECT user_id, token FROM user_token WHERE user_id = ? @@ -312,7 +310,7 @@ const ( // Schema management queries const ( - currentSchemaVersion = 5 + currentSchemaVersion = 4 insertSchemaVersion = `INSERT INTO schemaVersion VALUES (1, ?)` updateSchemaVersion = `UPDATE schemaVersion SET version = ? WHERE id = 1` selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` @@ -422,11 +420,6 @@ const ( FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE ); ` - - // 4 -> 5 - migrate4To5UpdateQueries = ` - UPDATE user_access SET topic = REPLACE(topic, '_', '\_'); - ` ) var ( @@ -434,7 +427,6 @@ var ( 1: migrateFrom1, 2: migrateFrom2, 3: migrateFrom3, - 4: migrateFrom4, } ) @@ -516,7 +508,7 @@ func (a *Manager) AuthenticateToken(token string) (*User, error) { // after a fixed duration unless ChangeToken is called. This function also prunes tokens for the // given user, if there are too many of them. func (a *Manager) CreateToken(userID, label string, expires time.Time, origin netip.Addr) (*Token, error) { - token := util.RandomLowerStringPrefix(tokenPrefix, tokenLength) // Lowercase only to support "+@" email addresses + token := util.RandomStringPrefix(tokenPrefix, tokenLength) tx, err := a.db.Begin() if err != nil { return nil, err @@ -541,7 +533,7 @@ func (a *Manager) CreateToken(userID, label string, expires time.Time, origin ne if tokenCount >= tokenMaxCount { // This pruning logic is done in two queries for efficiency. The SELECT above is a lookup // on two indices, whereas the query below is a full table scan. - if _, err := tx.Exec(deleteExcessTokensQuery, userID, userID, tokenMaxCount); err != nil { + if _, err := tx.Exec(deleteExcessTokensQuery, userID, tokenMaxCount); err != nil { return nil, err } } @@ -1129,7 +1121,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { return nil, err } reservations = append(reservations, Reservation{ - Topic: unescapeUnderscore(topic), + Topic: topic, Owner: NewPermission(ownerRead, ownerWrite), Everyone: NewPermission(everyoneRead.Bool, everyoneWrite.Bool), // false if null }) @@ -1139,7 +1131,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) { // HasReservation returns true if the given topic access is owned by the user func (a *Manager) HasReservation(username, topic string) (bool, error) { - rows, err := a.db.Query(selectUserHasReservationQuery, username, escapeUnderscore(topic)) + rows, err := a.db.Query(selectUserHasReservationQuery, username, topic) if err != nil { return false, err } @@ -1174,7 +1166,7 @@ func (a *Manager) ReservationsCount(username string) (int64, error) { // ReservationOwner returns user ID of the user that owns this topic, or an // empty string if it's not owned by anyone func (a *Manager) ReservationOwner(topic string) (string, error) { - rows, err := a.db.Query(selectUserReservationsOwnerQuery, escapeUnderscore(topic)) + rows, err := a.db.Query(selectUserReservationsOwnerQuery, topic) if err != nil { return "", err } @@ -1269,7 +1261,7 @@ func (a *Manager) AllowReservation(username string, topic string) error { if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) { return ErrInvalidArgument } - rows, err := a.db.Query(selectOtherAccessCountQuery, escapeUnderscore(topic), escapeUnderscore(topic), username) + rows, err := a.db.Query(selectOtherAccessCountQuery, topic, topic, username) if err != nil { return err } @@ -1334,10 +1326,10 @@ func (a *Manager) AddReservation(username string, topic string, everyone Permiss return err } defer tx.Rollback() - if _, err := tx.Exec(upsertUserAccessQuery, username, escapeUnderscore(topic), true, true, username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, username, topic, true, true, username, username); err != nil { return err } - if _, err := tx.Exec(upsertUserAccessQuery, Everyone, escapeUnderscore(topic), everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { + if _, err := tx.Exec(upsertUserAccessQuery, Everyone, topic, everyone.IsRead(), everyone.IsWrite(), username, username); err != nil { return err } return tx.Commit() @@ -1360,10 +1352,10 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error { } defer tx.Rollback() for _, topic := range topics { - if _, err := tx.Exec(deleteTopicAccessQuery, username, username, escapeUnderscore(topic)); err != nil { + if _, err := tx.Exec(deleteTopicAccessQuery, username, username, topic); err != nil { return err } - if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, escapeUnderscore(topic)); err != nil { + if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, topic); err != nil { return err } } @@ -1490,24 +1482,12 @@ func (a *Manager) Close() error { return a.db.Close() } -// toSQLWildcard converts a wildcard string to a SQL wildcard string. It only allows '*' as wildcards, -// and escapes '_', assuming '\' as escape character. func toSQLWildcard(s string) string { - return escapeUnderscore(strings.ReplaceAll(s, "*", "%")) + return strings.ReplaceAll(s, "*", "%") } -// fromSQLWildcard converts a SQL wildcard string to a wildcard string. It converts '%' to '*', -// and removes the '\_' escape character. func fromSQLWildcard(s string) string { - return strings.ReplaceAll(unescapeUnderscore(s), "%", "*") -} - -func escapeUnderscore(s string) string { - return strings.ReplaceAll(s, "_", "\\_") -} - -func unescapeUnderscore(s string) string { - return strings.ReplaceAll(s, "\\_", "_") + return strings.ReplaceAll(s, "%", "*") } func runStartupQueries(db *sql.DB, startupQueries string) error { @@ -1645,22 +1625,6 @@ func migrateFrom3(db *sql.DB) error { return tx.Commit() } -func migrateFrom4(db *sql.DB) error { - log.Tag(tag).Info("Migrating user database schema: from 4 to 5") - tx, err := db.Begin() - if err != nil { - return err - } - defer tx.Rollback() - if _, err := tx.Exec(migrate4To5UpdateQueries); err != nil { - return err - } - if _, err := tx.Exec(updateSchemaVersion, 5); err != nil { - return err - } - return tx.Commit() -} - func nullString(s string) sql.NullString { if s == "" { return sql.NullString{} diff --git a/user/manager_test.go b/user/manager_test.go index bebb0a07..5e01f497 100644 --- a/user/manager_test.go +++ b/user/manager_test.go @@ -3,10 +3,10 @@ package user import ( "database/sql" "fmt" - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" "github.com/stripe/stripe-go/v74" "golang.org/x/crypto/bcrypt" + "heckel.io/ntfy/util" "net/netip" "path/filepath" "strings" @@ -183,19 +183,6 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) { require.Equal(t, ErrUserNotFound, err) } -func TestManager_CreateToken_Only_Lower(t *testing.T) { - a := newTestManager(t, PermissionDenyAll) - - // Create user, add reservations and token - require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) - u, err := a.User("user") - require.Nil(t, err) - - token, err := a.CreateToken(u.ID, "", time.Now().Add(time.Hour), netip.IPv4Unspecified()) - require.Nil(t, err) - require.Equal(t, token.Value, strings.ToLower(token.Value)) -} - func TestManager_UserManagement(t *testing.T) { a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) @@ -330,7 +317,7 @@ func TestManager_Reservations(t *testing.T) { a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("phil", "phil", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll)) + require.Nil(t, a.AddReservation("ben", "ztopic", PermissionDenyAll)) require.Nil(t, a.AddReservation("ben", "readme", PermissionRead)) require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead)) @@ -343,7 +330,7 @@ func TestManager_Reservations(t *testing.T) { Everyone: PermissionRead, }, reservations[0]) require.Equal(t, Reservation{ - Topic: "ztopic_", + Topic: "ztopic", Owner: PermissionReadWrite, Everyone: PermissionDenyAll, }, reservations[1]) @@ -352,14 +339,6 @@ func TestManager_Reservations(t *testing.T) { require.Nil(t, err) require.True(t, b) - b, err = a.HasReservation("ben", "ztopic_") - require.Nil(t, err) - require.True(t, b) - - b, err = a.HasReservation("ben", "ztopicX") // _ != X (used to be a SQL wildcard issue) - require.Nil(t, err) - require.False(t, b) - b, err = a.HasReservation("notben", "readme") require.Nil(t, err) require.False(t, b) @@ -379,17 +358,11 @@ func TestManager_Reservations(t *testing.T) { err = a.AllowReservation("phil", "readme") require.Equal(t, errTopicOwnedByOthers, err) - err = a.AllowReservation("phil", "ztopic_") - require.Equal(t, errTopicOwnedByOthers, err) - - err = a.AllowReservation("phil", "ztopicX") - require.Nil(t, err) - err = a.AllowReservation("phil", "not-reserved") require.Nil(t, err) // Now remove them again - require.Nil(t, a.RemoveReservations("ben", "ztopic_", "readme")) + require.Nil(t, a.RemoveReservations("ben", "ztopic", "readme")) count, err = a.ReservationsCount("ben") require.Nil(t, err) @@ -594,80 +567,46 @@ func TestManager_Token_Extend(t *testing.T) { } func TestManager_Token_MaxCount_AutoDelete(t *testing.T) { - // Tests that tokens are automatically deleted when the maximum number of tokens is reached - a := newTestManager(t, PermissionDenyAll) require.Nil(t, a.AddUser("ben", "ben", RoleUser)) - require.Nil(t, a.AddUser("phil", "phil", RoleUser)) - ben, err := a.User("ben") + // Try to extend token for user without token + u, err := a.User("ben") require.Nil(t, err) - phil, err := a.User("phil") - require.Nil(t, err) - - // Create 2 tokens for phil - philTokens := make([]string, 0) - token, err := a.CreateToken(phil.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) - require.Nil(t, err) - require.NotEmpty(t, token.Value) - philTokens = append(philTokens, token.Value) - - token, err = a.CreateToken(phil.ID, "", time.Unix(0, 0), netip.IPv4Unspecified()) - require.Nil(t, err) - require.NotEmpty(t, token.Value) - philTokens = append(philTokens, token.Value) - - // Create 22 tokens for ben (only 20 allowed!) + // Tokens baseTime := time.Now().Add(24 * time.Hour) - benTokens := make([]string, 0) - for i := 0; i < 22; i++ { // - token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) + tokens := make([]string, 0) + for i := 0; i < 22; i++ { + token, err := a.CreateToken(u.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified()) require.Nil(t, err) require.NotEmpty(t, token.Value) - benTokens = append(benTokens, token.Value) + tokens = append(tokens, token.Value) // Manually modify expiry date to avoid sorting issues (this is a hack) _, err = a.db.Exec(`UPDATE user_token SET expires=? WHERE token=?`, baseTime.Add(time.Duration(i)*time.Minute).Unix(), token.Value) require.Nil(t, err) } - // Ben: The first 2 tokens should have been wiped and should not work anymore! - _, err = a.AuthenticateToken(benTokens[0]) + _, err = a.AuthenticateToken(tokens[0]) require.Equal(t, ErrUnauthenticated, err) - _, err = a.AuthenticateToken(benTokens[1]) + _, err = a.AuthenticateToken(tokens[1]) require.Equal(t, ErrUnauthenticated, err) - // Ben: The other tokens should still work for i := 2; i < 22; i++ { - userWithToken, err := a.AuthenticateToken(benTokens[i]) - require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i]) + userWithToken, err := a.AuthenticateToken(tokens[i]) + require.Nil(t, err, "token[%d]=%s failed", i, tokens[i]) require.Equal(t, "ben", userWithToken.Name) - require.Equal(t, benTokens[i], userWithToken.Token) + require.Equal(t, tokens[i], userWithToken.Token) } - // Phil: All tokens should still work - for i := 0; i < 2; i++ { - userWithToken, err := a.AuthenticateToken(philTokens[i]) - require.Nil(t, err, "token[%d]=%s failed", i, philTokens[i]) - require.Equal(t, "phil", userWithToken.Name) - require.Equal(t, philTokens[i], userWithToken.Token) - } - - var benCount int - rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, ben.ID) + var count int + rows, err := a.db.Query(`SELECT COUNT(*) FROM user_token`) require.Nil(t, err) require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&benCount)) - require.Equal(t, 20, benCount) - - var philCount int - rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID) - require.Nil(t, err) - require.True(t, rows.Next()) - require.Nil(t, rows.Scan(&philCount)) - require.Equal(t, 2, philCount) + require.Nil(t, rows.Scan(&count)) + require.Equal(t, 20, count) } func TestManager_EnqueueStats_ResetStats(t *testing.T) { @@ -992,44 +931,7 @@ func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) { require.Nil(t, a.AddPhoneNumber(ben.ID, "+1234567890")) } -func TestManager_Topic_Wildcard_With_Asterisk_Underscore(t *testing.T) { - f := filepath.Join(t.TempDir(), "user.db") - a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AllowAccess(Everyone, "*_", PermissionRead)) - require.Nil(t, a.AllowAccess(Everyone, "__*_", PermissionRead)) - require.Nil(t, a.Authorize(nil, "allowed_", PermissionRead)) - require.Nil(t, a.Authorize(nil, "__allowed_", PermissionRead)) - require.Nil(t, a.Authorize(nil, "_allowed_", PermissionRead)) // The "%" in "%\_" matches the first "_" - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "notallowed", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "_notallowed", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "__notallowed", PermissionRead)) -} - -func TestManager_Topic_Wildcard_With_Underscore(t *testing.T) { - f := filepath.Join(t.TempDir(), "user.db") - a := newTestManagerFromFile(t, f, "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval) - require.Nil(t, a.AllowAccess(Everyone, "mytopic_", PermissionReadWrite)) - require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead)) - require.Nil(t, a.Authorize(nil, "mytopic_", PermissionWrite)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionWrite)) -} - -func TestToFromSQLWildcard(t *testing.T) { - require.Equal(t, "up%", toSQLWildcard("up*")) - require.Equal(t, "up\\_%", toSQLWildcard("up_*")) - require.Equal(t, "foo", toSQLWildcard("foo")) - - require.Equal(t, "up*", fromSQLWildcard("up%")) - require.Equal(t, "up_*", fromSQLWildcard("up\\_%")) - require.Equal(t, "foo", fromSQLWildcard("foo")) - - require.Equal(t, "up*", fromSQLWildcard(toSQLWildcard("up*"))) - require.Equal(t, "up_*", fromSQLWildcard(toSQLWildcard("up_*"))) - require.Equal(t, "foo", fromSQLWildcard(toSQLWildcard("foo"))) -} - -func TestMigrationFrom1(t *testing.T) { +func TestSqliteCache_Migration_From1(t *testing.T) { filename := filepath.Join(t.TempDir(), "user.db") db, err := sql.Open("sqlite3", filename) require.Nil(t, err) @@ -1114,152 +1016,6 @@ func TestMigrationFrom1(t *testing.T) { require.Equal(t, PermissionRead, everyoneGrants[0].Allow) } -func TestMigrationFrom4(t *testing.T) { - filename := filepath.Join(t.TempDir(), "user.db") - db, err := sql.Open("sqlite3", filename) - require.Nil(t, err) - - // Create "version 4" schema - _, err = db.Exec(` - BEGIN; - CREATE TABLE IF NOT EXISTS tier ( - id TEXT PRIMARY KEY, - code TEXT NOT NULL, - name TEXT NOT NULL, - messages_limit INT NOT NULL, - messages_expiry_duration INT NOT NULL, - emails_limit INT NOT NULL, - calls_limit INT NOT NULL, - reservations_limit INT NOT NULL, - attachment_file_size_limit INT NOT NULL, - attachment_total_size_limit INT NOT NULL, - attachment_expiry_duration INT NOT NULL, - attachment_bandwidth_limit INT NOT NULL, - stripe_monthly_price_id TEXT, - stripe_yearly_price_id TEXT - ); - CREATE UNIQUE INDEX idx_tier_code ON tier (code); - CREATE UNIQUE INDEX idx_tier_stripe_monthly_price_id ON tier (stripe_monthly_price_id); - CREATE UNIQUE INDEX idx_tier_stripe_yearly_price_id ON tier (stripe_yearly_price_id); - CREATE TABLE IF NOT EXISTS user ( - id TEXT PRIMARY KEY, - tier_id TEXT, - user TEXT NOT NULL, - pass TEXT NOT NULL, - role TEXT CHECK (role IN ('anonymous', 'admin', 'user')) NOT NULL, - prefs JSON NOT NULL DEFAULT '{}', - sync_topic TEXT NOT NULL, - stats_messages INT NOT NULL DEFAULT (0), - stats_emails INT NOT NULL DEFAULT (0), - stats_calls INT NOT NULL DEFAULT (0), - stripe_customer_id TEXT, - stripe_subscription_id TEXT, - stripe_subscription_status TEXT, - stripe_subscription_interval TEXT, - stripe_subscription_paid_until INT, - stripe_subscription_cancel_at INT, - created INT NOT NULL, - deleted INT, - FOREIGN KEY (tier_id) REFERENCES tier (id) - ); - CREATE UNIQUE INDEX idx_user ON user (user); - CREATE UNIQUE INDEX idx_user_stripe_customer_id ON user (stripe_customer_id); - CREATE UNIQUE INDEX idx_user_stripe_subscription_id ON user (stripe_subscription_id); - CREATE TABLE IF NOT EXISTS user_access ( - user_id TEXT NOT NULL, - topic TEXT NOT NULL, - read INT NOT NULL, - write INT NOT NULL, - owner_user_id INT, - PRIMARY KEY (user_id, topic), - FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE, - FOREIGN KEY (owner_user_id) REFERENCES user (id) ON DELETE CASCADE - ); - CREATE TABLE IF NOT EXISTS user_token ( - user_id TEXT NOT NULL, - token TEXT NOT NULL, - label TEXT NOT NULL, - last_access INT NOT NULL, - last_origin TEXT NOT NULL, - expires INT NOT NULL, - PRIMARY KEY (user_id, token), - FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE - ); - CREATE TABLE IF NOT EXISTS user_phone ( - user_id TEXT NOT NULL, - phone_number TEXT NOT NULL, - PRIMARY KEY (user_id, phone_number), - FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE - ); - CREATE TABLE IF NOT EXISTS schemaVersion ( - id INT PRIMARY KEY, - version INT NOT NULL - ); - INSERT INTO user (id, user, pass, role, sync_topic, created) - VALUES ('u_everyone', '*', '', 'anonymous', '', UNIXEPOCH()) - ON CONFLICT (id) DO NOTHING; - INSERT INTO schemaVersion (id, version) VALUES (1, 4); - COMMIT; - `) - require.Nil(t, err) - - // Insert a few ACL entries - _, err = db.Exec(` - BEGIN; - INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'mytopic_', 1, 1); - INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'up%', 1, 1); - INSERT INTO user_access (user_id, topic, read, write) values ('u_everyone', 'down_%', 1, 1); - COMMIT; - `) - require.Nil(t, err) - - // Create manager to trigger migration - a := newTestManagerFromFile(t, filename, "", PermissionDenyAll, bcrypt.MinCost, DefaultUserStatsQueueWriterInterval) - checkSchemaVersion(t, a.db) - - // Add another - require.Nil(t, a.AllowAccess(Everyone, "left_*", PermissionReadWrite)) - - // Check "external view" of grants - everyoneGrants, err := a.Grants(Everyone) - require.Nil(t, err) - - require.Equal(t, 4, len(everyoneGrants)) - require.Equal(t, "down_*", everyoneGrants[0].TopicPattern) - require.Equal(t, "left_*", everyoneGrants[1].TopicPattern) - require.Equal(t, "mytopic_", everyoneGrants[2].TopicPattern) - require.Equal(t, "up*", everyoneGrants[3].TopicPattern) - - // Check they are stored correctly in the database - rows, err := db.Query(`SELECT topic FROM user_access WHERE user_id = 'u_everyone' ORDER BY topic`) - require.Nil(t, err) - topicPatterns := make([]string, 0) - for rows.Next() { - var topicPattern string - require.Nil(t, rows.Scan(&topicPattern)) - topicPatterns = append(topicPatterns, topicPattern) - } - require.Nil(t, rows.Close()) - require.Equal(t, 4, len(topicPatterns)) - require.Equal(t, "down\\_%", topicPatterns[0]) - require.Equal(t, "left\\_%", topicPatterns[1]) - require.Equal(t, "mytopic\\_", topicPatterns[2]) - require.Equal(t, "up%", topicPatterns[3]) - - // Check that ACL works as excepted - require.Nil(t, a.Authorize(nil, "down_123", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "downX123", PermissionRead)) - - require.Nil(t, a.Authorize(nil, "left_abc", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "leftX123", PermissionRead)) - - require.Nil(t, a.Authorize(nil, "mytopic_", PermissionRead)) - require.Equal(t, ErrUnauthorized, a.Authorize(nil, "mytopicX", PermissionRead)) - - require.Nil(t, a.Authorize(nil, "up123", PermissionRead)) - require.Nil(t, a.Authorize(nil, "up", PermissionRead)) // % matches 0 or more characters -} - func checkSchemaVersion(t *testing.T, db *sql.DB) { rows, err := db.Query(`SELECT version FROM schemaVersion`) require.Nil(t, err) diff --git a/user/types.go b/user/types.go index 140da216..11895785 100644 --- a/user/types.go +++ b/user/types.go @@ -2,8 +2,8 @@ package user import ( "errors" - "git.zio.sh/astra/ntfy/v2/log" "github.com/stripe/stripe-go/v74" + "heckel.io/ntfy/log" "net/netip" "regexp" "strings" diff --git a/util/batching_queue_test.go b/util/batching_queue_test.go index 08d812ed..b3c41a4c 100644 --- a/util/batching_queue_test.go +++ b/util/batching_queue_test.go @@ -1,8 +1,8 @@ package util_test import ( - "git.zio.sh/astra/ntfy/v2/util" "github.com/stretchr/testify/require" + "heckel.io/ntfy/util" "math/rand" "sync" "testing" diff --git a/util/util.go b/util/util.go index d48487df..84177d9f 100644 --- a/util/util.go +++ b/util/util.go @@ -23,8 +23,7 @@ import ( ) const ( - randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - randomStringLowerCaseCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) var ( @@ -113,20 +112,11 @@ func RandomString(length int) string { // RandomStringPrefix returns a random string with a given length, with a prefix func RandomStringPrefix(prefix string, length int) string { - return randomStringPrefixWithCharset(prefix, length, randomStringCharset) -} - -// RandomLowerStringPrefix returns a random lowercase-only string with a given length, with a prefix -func RandomLowerStringPrefix(prefix string, length int) string { - return randomStringPrefixWithCharset(prefix, length, randomStringLowerCaseCharset) -} - -func randomStringPrefixWithCharset(prefix string, length int, charset string) string { randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?! defer randomMutex.Unlock() b := make([]byte, length-len(prefix)) for i := range b { - b[i] = charset[random.Intn(len(charset))] + b[i] = randomStringCharset[random.Intn(len(randomStringCharset))] } return prefix + string(b) } @@ -161,6 +151,11 @@ func ParsePriority(priority string) (int, error) { case "5", "max", "urgent": return 5, nil default: + // Ignore new HTTP Priority header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority) + // Cloudflare adds this to requests when forwarding to the backend (ntfy), so we just ignore it. + if strings.HasPrefix(p, "u=") { + return 3, nil + } return 0, errInvalidPriority } } diff --git a/util/util_test.go b/util/util_test.go index f0f45c28..49a24126 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -87,6 +87,15 @@ func TestParsePriority_Invalid(t *testing.T) { } } +func TestParsePriority_HTTPSpecPriority(t *testing.T) { + priorities := []string{"u=1", "u=3", "u=7, i"} // see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority + for _, priority := range priorities { + actual, err := ParsePriority(priority) + require.Nil(t, err) + require.Equal(t, 3, actual) // Always expect 3! + } +} + func TestPriorityString(t *testing.T) { priorities := []int{0, 1, 2, 3, 4, 5} expected := []string{"default", "min", "low", "default", "high", "max"} diff --git a/web/.eslintrc b/web/.eslintrc index a21221fc..adf66130 100644 --- a/web/.eslintrc +++ b/web/.eslintrc @@ -33,6 +33,5 @@ "unnamedComponents": "arrow-function" } ] - }, - "overrides": [{ "files": ["./public/sw.js"], "rules": { "no-restricted-globals": "off" } }] + } } diff --git a/web/index.html b/web/index.html index 191e8c40..c146e64d 100644 --- a/web/index.html +++ b/web/index.html @@ -13,24 +13,18 @@ - - - @@ -41,9 +35,6 @@ - - -