diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3bf2a12
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+dist
+*/node_modules
+Dockerfile*
diff --git a/.gitignore b/.gitignore
index b0c2d33..f695607 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
dist/
+dev-dist/
build/
.idea/
.vscode/
diff --git a/Dockerfile b/Dockerfile
index 7c2052e..feb813f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine
+FROM r.batts.cloud/debian:testing
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
diff --git a/Dockerfile-build b/Dockerfile-build
new file mode 100644
index 0000000..9c6d1bc
--- /dev/null
+++ b/Dockerfile-build
@@ -0,0 +1,54 @@
+FROM r.batts.cloud/golang:1.19 as builder
+
+ARG VERSION=dev
+ARG COMMIT=unknown
+
+RUN apt-get update
+RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
+RUN apt-get install -y \
+ build-essential \
+ nodejs \
+ python3-pip \
+ python3-venv
+
+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 r.batts.cloud/debian:testing
+
+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 7398844..440bfa6 100644
--- a/Makefile
+++ b/Makefile
@@ -31,12 +31,16 @@ 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-format - Run prettier on the web app
- @echo " make web-format-check - Run prettier on the web app, but don't change anything
+ @echo " make web-lint - Run eslint on the web app"
+ @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"
@@ -82,23 +86,33 @@ 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 update
- sudo apt install -y \
+ sudo apt-get update
+ sudo apt-get install -y \
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
jq
- which pip3 || sudo apt install -y python3-pip
+ which pip3 || sudo apt-get install -y python3-pip
# Documentation
docs: docs-deps docs-build
-docs-build: .PHONY
- @if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
+docs-build: venv .PHONY
+ @. venv/bin/activate && \
+ 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; \
@@ -111,10 +125,15 @@ docs-build: .PHONY
mkdocs build; \
fi
-docs-deps: .PHONY
+venv:
+ python3 -m venv ./venv
+
+docs-deps: venv .PHONY
+ . venv/bin/activate && \
pip3 install -r requirements.txt
-docs-deps-update: .PHONY
+docs-deps-update: venv .PHONY
+ . venv/bin/activate && \
pip3 install -r requirements.txt --upgrade
diff --git a/client/client.go b/client/client.go
index b744fa1..93cf7da 100644
--- a/client/client.go
+++ b/client/client.go
@@ -11,23 +11,25 @@ import (
"heckel.io/ntfy/util"
"io"
"net/http"
+ "regexp"
"strings"
"sync"
"time"
)
-// Event type constants
const (
- MessageEvent = "message"
- KeepaliveEvent = "keepalive"
- OpenEvent = "open"
- PollRequestEvent = "poll_request"
+ // MessageEvent identifies a message event
+ MessageEvent = "message"
)
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
@@ -96,8 +98,14 @@ 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 := c.expandTopicURL(topic)
- req, _ := http.NewRequest("POST", topicURL, body)
+ topicURL, err := c.expandTopicURL(topic)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest("POST", topicURL, body)
+ if err != nil {
+ return nil, err
+ }
for _, option := range options {
if err := option(req); err != nil {
return nil, err
@@ -133,11 +141,14 @@ 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() {
@@ -166,15 +177,18 @@ 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 {
+func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
+ topicURL, err := c.expandTopicURL(topic)
+ if err != nil {
+ return "", err
+ }
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{
@@ -183,7 +197,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
cancel: cancel,
}
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
- return subscriptionID
+ return subscriptionID, nil
}
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
@@ -199,31 +213,16 @@ func (c *Client) Unsubscribe(subscriptionID string) {
sub.cancel()
}
-// 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 {
+func (c *Client) expandTopicURL(topic string) (string, error) {
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
- return topic
+ return topic, nil
} else if strings.Contains(topic, "/") {
- return fmt.Sprintf("https://%s", topic)
+ return fmt.Sprintf("https://%s", topic), nil
}
- return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
+ if !topicRegex.MatchString(topic) {
+ return "", fmt.Errorf("invalid topic name: %s", topic)
+ }
+ return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
}
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
diff --git a/client/client_test.go b/client/client_test.go
index a71ea5c..f0b15a3 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -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/cmd/subscribe.go b/cmd/subscribe.go
index 2691e6a..c85c468 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -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,7 +194,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
topicOptions = append(topicOptions, auth)
}
- subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
+ subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
+ if err != nil {
+ return err
+ }
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
@@ -204,7 +207,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
}
}
if topic != "" {
- subscriptionID := cl.Subscribe(topic, options...)
+ subscriptionID, err := cl.Subscribe(topic, options...)
+ if err != nil {
+ return err
+ }
cmds[subscriptionID] = command
}
for m := range cl.Messages {
diff --git a/docs/develop.md b/docs/develop.md
index a53c503..baab3f3 100644
--- a/docs/develop.md
+++ b/docs/develop.md
@@ -163,6 +163,15 @@ $ 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:
diff --git a/docs/releases.md b/docs/releases.md
index 52fba34..0e93b67 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1225,9 +1225,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**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)
**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/server/server.go b/server/server.go
index ac54aa5..d2fac01 100644
--- a/server/server.go
+++ b/server/server.go
@@ -760,7 +760,7 @@ 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 != "" {
+ if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
go s.forwardPollRequest(v, m)
}
} else {
diff --git a/server/server_test.go b/server/server_test.go
index 73df276..d7c4a7c 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -2559,6 +2559,29 @@ 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"
diff --git a/web/package-lock.json b/web/package-lock.json
index 3781278..b5754d9 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -405,25 +405,6 @@
"stylis": "4.2.0"
}
},
- "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@emotion/babel-plugin/node_modules/source-map": {
- "version": "0.5.7",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
- "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/@emotion/cache": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
@@ -1708,6 +1689,14 @@
"node": ">=4"
}
},
+ "node_modules/chalk/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
@@ -2084,11 +2073,14 @@
}
},
"node_modules/escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
- "node": ">=0.8.0"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
@@ -2472,18 +2464,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "node_modules/eslint/node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/eslint/node_modules/globals": {
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
@@ -4202,9 +4182,9 @@
}
},
"node_modules/source-map": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
- "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
@@ -4240,6 +4220,14 @@
"stackframe": "^1.3.4"
}
},
+ "node_modules/stacktrace-gps/node_modules/source-map": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
+ "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/stacktrace-js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
diff --git a/web/public/static/langs/cs.json b/web/public/static/langs/cs.json
index aeff195..6b967c8 100644
--- a/web/public/static/langs/cs.json
+++ b/web/public/static/langs/cs.json
@@ -355,5 +355,15 @@
"account_upgrade_dialog_billing_contact_website": "Otázky týkající se fakturace naleznete na našich webových stránkách.",
"account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} rezervované téma",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} denní zpráva",
- "account_upgrade_dialog_tier_features_emails_one": "{{emails}} denní e-mail"
+ "account_upgrade_dialog_tier_features_emails_one": "{{emails}} denní e-mail",
+ "publish_dialog_call_label": "Telefonát",
+ "publish_dialog_call_reset": "Odstranit telefonát",
+ "publish_dialog_chip_call_label": "Telefonát",
+ "account_basics_phone_numbers_title": "Telefonní čísla",
+ "account_basics_phone_numbers_dialog_description": "Pro oznámení prostřednictvím tel. hovoru, musíte přidat a ověřit alespoň jedno telefonní číslo. Ověření lze provést pomocí SMS nebo telefonátu.",
+ "account_basics_phone_numbers_description": "K oznámení telefonátem",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Zatím žádná telefonní čísla",
+ "account_basics_phone_numbers_copied_to_clipboard": "Telefonní číslo zkopírováno do schránky",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Žádná ověřená telefonní čísla",
+ "publish_dialog_call_item": "Vytočit číslo {{number}}"
}
diff --git a/web/public/static/langs/es.json b/web/public/static/langs/es.json
index 3166a52..62ecdaf 100644
--- a/web/public/static/langs/es.json
+++ b/web/public/static/langs/es.json
@@ -355,5 +355,31 @@
"account_upgrade_dialog_billing_contact_email": "Para preguntas sobre facturación, por favor contáctenos directamente.",
"account_upgrade_dialog_tier_features_messages_one": "{{messages}} mensaje diario",
"account_upgrade_dialog_tier_features_emails_one": "{{emails}} correo electrónico diario",
- "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado"
+ "account_upgrade_dialog_tier_features_reservations_one": "{{reservations}} tema reservado",
+ "publish_dialog_call_label": "Llamada telefónica",
+ "publish_dialog_call_placeholder": "Número de teléfono al cual llamar con el mensaje, por ejemplo +12223334444, o \"sí\"",
+ "publish_dialog_chip_call_label": "Llamada telefónica",
+ "account_basics_phone_numbers_title": "Números de teléfono",
+ "account_basics_phone_numbers_description": "Para notificaciones por llamada teléfonica",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Aún no hay números de teléfono",
+ "account_basics_phone_numbers_dialog_number_label": "Número de teléfono",
+ "account_basics_phone_numbers_dialog_number_placeholder": "p. ej. +1222333444",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Envía SMS",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Llámame",
+ "account_basics_phone_numbers_dialog_code_label": "Código de verificación",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_basics_phone_numbers_dialog_channel_call": "Llamar",
+ "account_usage_calls_title": "Llamadas telefónicas realizadas",
+ "account_usage_calls_none": "No se pueden hacer llamadas telefónicas con esta cuenta",
+ "account_upgrade_dialog_tier_features_calls_one": "{{llamadas}} llamadas telefónicas diarias",
+ "account_upgrade_dialog_tier_features_calls_other": "{{llamadas}} llamadas telefónicas diarias",
+ "account_upgrade_dialog_tier_features_no_calls": "No hay llamadas telefónicas",
+ "publish_dialog_call_reset": "Eliminar llamada telefónica",
+ "account_basics_phone_numbers_dialog_description": "Para utilizar la función de notificación de llamadas, tiene que añadir y verificar al menos un número de teléfono. La verificación puede realizarse mediante un SMS o una llamada telefónica.",
+ "account_basics_phone_numbers_copied_to_clipboard": "Número de teléfono copiado al portapapeles",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Confirmar código",
+ "account_basics_phone_numbers_dialog_title": "Agregar número de teléfono",
+ "account_basics_phone_numbers_dialog_code_placeholder": "p.ej. 123456",
+ "publish_dialog_call_item": "Llamar al número de teléfono {{number}}",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "No hay números de teléfono verificados"
}
diff --git a/web/public/static/langs/id.json b/web/public/static/langs/id.json
index 76c9d1d..48fcda0 100644
--- a/web/public/static/langs/id.json
+++ b/web/public/static/langs/id.json
@@ -379,5 +379,7 @@
"account_basics_phone_numbers_dialog_channel_sms": "SMS",
"account_upgrade_dialog_tier_features_calls_one": "{{calls}} panggilan telepon harian",
"account_upgrade_dialog_tier_features_no_calls": "Tidak ada panggilan telepon",
- "account_basics_phone_numbers_dialog_code_label": "Kode verifikasi"
+ "account_basics_phone_numbers_dialog_code_label": "Kode verifikasi",
+ "publish_dialog_call_item": "Panggil nomor telepon {{number}}",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Tidak ada nomor telepon terverifikasi"
}
diff --git a/web/public/static/langs/nl.json b/web/public/static/langs/nl.json
index ca7a2a1..8ccb629 100644
--- a/web/public/static/langs/nl.json
+++ b/web/public/static/langs/nl.json
@@ -355,5 +355,30 @@
"prefs_reservations_table_topic_header": "Onderwerp",
"prefs_reservations_table_access_header": "Toegang",
"prefs_reservations_table_everyone_read_write": "Iedereen kan publiceren en abonneren",
- "prefs_reservations_table_not_subscribed": "Niet geabonneerd"
+ "prefs_reservations_table_not_subscribed": "Niet geabonneerd",
+ "publish_dialog_call_label": "Telefoongesprek",
+ "publish_dialog_call_reset": "Telefoongesprek verwijderen",
+ "publish_dialog_chip_call_label": "Telefoongesprek",
+ "account_basics_phone_numbers_title": "Telefoonnummers",
+ "account_basics_phone_numbers_description": "Voor meldingen via telefoongesprekken",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Nog geen telefoonnummers",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Bel me",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} dagelijkse telefoontjes",
+ "account_basics_phone_numbers_copied_to_clipboard": "Telefoonnummer gekopieerd naar klembord",
+ "publish_dialog_call_item": "Bel telefoonnummer {{nummer}}",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Bevestig code",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Geen geverifieerde telefoonnummers",
+ "account_basics_phone_numbers_dialog_channel_call": "Telefoongesprek",
+ "account_basics_phone_numbers_dialog_number_label": "Telefoonnummer",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_basics_phone_numbers_dialog_code_placeholder": "bijv. 123456",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} dagelijkse telefoontjes",
+ "account_upgrade_dialog_tier_features_no_calls": "Geen telefoontjes",
+ "account_basics_phone_numbers_dialog_description": "Als u de functie voor oproepmeldingen wilt gebruiken, moet u ten minste één telefoonnummer toevoegen en verifiëren. Verificatie kan worden gedaan via sms of een telefoontje.",
+ "account_basics_phone_numbers_dialog_title": "Telefoonnummer toevoegen",
+ "account_basics_phone_numbers_dialog_number_placeholder": "bijv. +1222333444",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Stuur SMS",
+ "account_basics_phone_numbers_dialog_code_label": "Verificatiecode",
+ "account_usage_calls_title": "Aantal telefoontjes",
+ "account_usage_calls_none": "Met dit account kan niet worden gebeld"
}
diff --git a/web/public/static/langs/pt.json b/web/public/static/langs/pt.json
index bf753c9..57d5656 100644
--- a/web/public/static/langs/pt.json
+++ b/web/public/static/langs/pt.json
@@ -214,5 +214,17 @@
"login_link_signup": "Registar",
"action_bar_reservation_add": "Reservar tópico",
"action_bar_sign_up": "Registar",
- "nav_button_account": "Conta"
+ "nav_button_account": "Conta",
+ "common_copy_to_clipboard": "Copiar",
+ "nav_upgrade_banner_label": "Atualizar para ntfy Pro",
+ "alert_not_supported_context_description": "Notificações são suportadas apenas sobre HTTPS. Essa é uma limitação da API de Notificações.",
+ "display_name_dialog_title": "Alterar nome mostrado",
+ "display_name_dialog_description": "Configura um nome alternativo ao tópico que é mostrado na lista de assinaturas. Isto ajuda a identificar tópicos com nomes complicados mais facilmente.",
+ "display_name_dialog_placeholder": "Nome exibido",
+ "reserve_dialog_checkbox_label": "Reservar tópico e configurar acesso",
+ "publish_dialog_call_label": "Chamada telefônica",
+ "publish_dialog_call_placeholder": "Número de telefone para ligar com a mensagem, ex: +12223334444, ou 'Sim'",
+ "publish_dialog_call_reset": "Remover chamada telefônica",
+ "publish_dialog_chip_call_label": "Chamada telefônica",
+ "subscribe_dialog_subscribe_button_generate_topic_name": "Gerar nome"
}
diff --git a/web/public/static/langs/sv.json b/web/public/static/langs/sv.json
index 31e809c..bc4a540 100644
--- a/web/public/static/langs/sv.json
+++ b/web/public/static/langs/sv.json
@@ -355,5 +355,30 @@
"reservation_delete_dialog_action_keep_title": "Behåll cachade meddelanden och bilagor",
"reservation_delete_dialog_action_keep_description": "Meddelanden och bilagor som lagras på servern blir offentligt synliga för personer som känner till ämnesnamnet.",
"reservation_delete_dialog_action_delete_title": "Ta bort meddelanden och bilagor som sparats i cacheminnet",
- "reservation_delete_dialog_description": "Om du tar bort en reservation ger du upp äganderätten till ämnet och låter andra reservera det. Du kan behålla eller radera befintliga meddelanden och bilagor."
+ "reservation_delete_dialog_description": "Om du tar bort en reservation ger du upp äganderätten till ämnet och låter andra reservera det. Du kan behålla eller radera befintliga meddelanden och bilagor.",
+ "publish_dialog_call_label": "Telefonsamtal",
+ "publish_dialog_call_reset": "Ta bort telefonsamtal",
+ "publish_dialog_chip_call_label": "Telefonsamtal",
+ "account_basics_phone_numbers_title": "Telefonnummer",
+ "account_basics_phone_numbers_description": "För notifieringar via telefonsamtal",
+ "account_basics_phone_numbers_no_phone_numbers_yet": "Inga telefonnummer ännu",
+ "account_basics_phone_numbers_copied_to_clipboard": "Telefonnummer kopierat till urklipp",
+ "account_basics_phone_numbers_dialog_title": "Lägga till telefonnummer",
+ "account_basics_phone_numbers_dialog_number_label": "Telefonnummer",
+ "account_basics_phone_numbers_dialog_number_placeholder": "t.ex. +1222333444",
+ "account_basics_phone_numbers_dialog_verify_button_sms": "Skicka SMS",
+ "account_basics_phone_numbers_dialog_verify_button_call": "Ring mig",
+ "account_basics_phone_numbers_dialog_code_label": "Verifieringskod",
+ "account_basics_phone_numbers_dialog_channel_call": "Ring",
+ "account_usage_calls_title": "Telefonsamtal som gjorts",
+ "account_usage_calls_none": "Inga telefonsamtal kan göras med detta konto",
+ "publish_dialog_call_item": "Ring telefonnummer {{number}}",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Inga verifierade telefonnummer",
+ "account_basics_phone_numbers_dialog_description": "För att använda funktionen för samtalsavisering måste du lägga till och verifiera minst ett telefonnummer. Verifieringen kan göras via SMS eller ett telefonsamtal.",
+ "account_basics_phone_numbers_dialog_code_placeholder": "t.ex. 123456",
+ "account_basics_phone_numbers_dialog_check_verification_button": "Bekräfta kod",
+ "account_basics_phone_numbers_dialog_channel_sms": "SMS",
+ "account_upgrade_dialog_tier_features_calls_other": "{{calls}} dagliga telefonsamtal",
+ "account_upgrade_dialog_tier_features_no_calls": "Inga telefonsamtal",
+ "account_upgrade_dialog_tier_features_calls_one": "{{calls}} dagliga telefonsamtal"
}
diff --git a/web/public/static/langs/uk.json b/web/public/static/langs/uk.json
index 7d995dd..32a3079 100644
--- a/web/public/static/langs/uk.json
+++ b/web/public/static/langs/uk.json
@@ -352,5 +352,34 @@
"account_upgrade_dialog_reservations_warning_other": "Обраний рівень дозволяє менше зарезервованих тем, ніж ваш поточний рівень. Перш ніж змінити свій рівень, будь ласка, видаліть принаймні {{count}} резервувань. Ви можете видалити резервування в Налаштуваннях.",
"account_upgrade_dialog_button_cancel": "Скасувати",
"account_upgrade_dialog_button_redirect_signup": "Зареєструватися зараз",
- "account_upgrade_dialog_button_pay_now": "Оплатити зараз і підписатися"
+ "account_upgrade_dialog_button_pay_now": "Оплатити зараз і підписатися",
+ "prefs_reservations_add_button": "Додати зарезервовану тему",
+ "prefs_reservations_edit_button": "Редагувати доступ до теми",
+ "prefs_reservations_limit_reached": "Ви досягли ліміту зарезервованих тем.",
+ "prefs_reservations_table_click_to_subscribe": "Натисніть, щоб підписатися",
+ "prefs_reservations_table_topic_header": "Тема",
+ "prefs_reservations_description": "Тут ви можете зарезервувати назви тем для особистого користування. Резервування теми дає вам право власності на тему і дозволяє визначати права доступу до неї інших користувачів.",
+ "prefs_reservations_table": "Таблиця зарезервованих тем",
+ "prefs_reservations_table_access_header": "Доступ",
+ "prefs_reservations_table_everyone_deny_all": "Тільки я можу публікувати та підписуватись",
+ "prefs_reservations_table_everyone_read_only": "Я можу публікувати та підписуватись, кожен може підписатися",
+ "prefs_reservations_table_everyone_write_only": "Я можу публікувати і підписуватися, кожен може публікувати",
+ "prefs_reservations_table_everyone_read_write": "Кожен може публікувати та підписуватися",
+ "prefs_reservations_table_not_subscribed": "Не підписаний",
+ "prefs_reservations_dialog_title_add": "Зарезервувати тему",
+ "prefs_reservations_dialog_title_edit": "Редагувати зарезервовану тему",
+ "prefs_reservations_title": "Зарезервовані теми",
+ "prefs_reservations_delete_button": "Скинути доступ до теми",
+ "prefs_reservations_dialog_description": "Резервування теми дає вам право власності на цю тему і дозволяє визначати права доступу до неї інших користувачів.",
+ "prefs_reservations_dialog_topic_label": "Тема",
+ "prefs_reservations_dialog_access_label": "Доступ",
+ "reservation_delete_dialog_description": "Видалення резервування позбавляє вас права власності на тему і дозволяє іншим зарезервувати її. Ви можете зберегти або видалити існуючі повідомлення і вкладення.",
+ "reservation_delete_dialog_submit_button": "Видалити резервування",
+ "publish_dialog_call_item": "Телефонувати за номером {{номер}}",
+ "publish_dialog_chip_call_no_verified_numbers_tooltip": "Немає підтверджених номерів телефонів",
+ "prefs_reservations_dialog_title_delete": "Видалити резервування теми",
+ "reservation_delete_dialog_action_delete_title": "Видалення кешованих повідомлень і вкладень",
+ "reservation_delete_dialog_action_keep_title": "Збереження кешованих повідомлень і вкладень",
+ "reservation_delete_dialog_action_keep_description": "Повідомлення і вкладення, які кешуються на сервері, стають загальнодоступними для людей, які знають назву теми.",
+ "reservation_delete_dialog_action_delete_description": "Кешовані повідомлення та вкладення будуть видалені назавжди. Ця дія не може бути скасована."
}
diff --git a/web/src/app/ConnectionManager.js b/web/src/app/ConnectionManager.js
index 751c7bd..2033cbe 100644
--- a/web/src/app/ConnectionManager.js
+++ b/web/src/app/ConnectionManager.js
@@ -61,9 +61,7 @@ class ConnectionManager {
const { connectionId } = subscription;
const added = !this.connections.get(connectionId);
if (added) {
- const { baseUrl } = subscription;
- const { topic } = subscription;
- const { user } = subscription;
+ const { baseUrl, topic, user } = subscription;
const since = subscription.last;
const connection = new Connection(
connectionId,
diff --git a/web/src/app/Poller.js b/web/src/app/Poller.js
index b956821..372e46e 100644
--- a/web/src/app/Poller.js
+++ b/web/src/app/Poller.js
@@ -21,15 +21,16 @@ class Poller {
async pollAll() {
console.log(`[Poller] Polling all subscriptions`);
const subscriptions = await subscriptionManager.all();
- for (const s of subscriptions) {
- try {
- // TODO(eslint): Switch to Promise.all
- // eslint-disable-next-line no-await-in-loop
- await this.poll(s);
- } catch (e) {
- console.log(`[Poller] Error polling ${s.id}`, e);
- }
- }
+
+ await Promise.all(
+ subscriptions.map(async (s) => {
+ try {
+ await this.poll(s);
+ } catch (e) {
+ console.log(`[Poller] Error polling ${s.id}`, e);
+ }
+ })
+ );
}
async poll(subscription) {
diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js
index 7762753..ecbe4da 100644
--- a/web/src/app/SubscriptionManager.js
+++ b/web/src/app/SubscriptionManager.js
@@ -5,13 +5,12 @@ class SubscriptionManager {
/** All subscriptions, including "new count"; this is a JOIN, see https://dexie.org/docs/API-Reference#joining */
async all() {
const subscriptions = await db.subscriptions.toArray();
- await Promise.all(
- subscriptions.map(async (s) => {
- // eslint-disable-next-line no-param-reassign
- s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
- })
+ return Promise.all(
+ subscriptions.map(async (s) => ({
+ ...s,
+ new: await db.notifications.where({ subscriptionId: s.id, new: 1 }).count(),
+ }))
);
- return subscriptions;
}
async get(subscriptionId) {
@@ -40,33 +39,31 @@ class SubscriptionManager {
console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions);
// Add remote subscriptions
- const remoteIds = []; // = topicUrl(baseUrl, topic)
- for (let i = 0; i < remoteSubscriptions.length; i += 1) {
- const remote = remoteSubscriptions[i];
- // TODO(eslint): Switch to Promise.all
- // eslint-disable-next-line no-await-in-loop
- const local = await this.add(remote.base_url, remote.topic, false);
- const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
- // TODO(eslint): Switch to Promise.all
- // eslint-disable-next-line no-await-in-loop
- await this.update(local.id, {
- displayName: remote.display_name, // May be undefined
- reservation, // May be null!
- });
- remoteIds.push(local.id);
- }
+ const remoteIds = await Promise.all(
+ remoteSubscriptions.map(async (remote) => {
+ const local = await this.add(remote.base_url, remote.topic, false);
+ const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
+
+ await this.update(local.id, {
+ displayName: remote.display_name, // May be undefined
+ reservation, // May be null!
+ });
+
+ return local.id;
+ })
+ );
// Remove local subscriptions that do not exist remotely
const localSubscriptions = await db.subscriptions.toArray();
- for (let i = 0; i < localSubscriptions.length; i += 1) {
- const local = localSubscriptions[i];
- const remoteExists = remoteIds.includes(local.id);
- if (!local.internal && !remoteExists) {
- // TODO(eslint): Switch to Promise.all
- // eslint-disable-next-line no-await-in-loop
- await this.remove(local.id);
- }
- }
+
+ await Promise.all(
+ localSubscriptions.map(async (local) => {
+ const remoteExists = remoteIds.includes(local.id);
+ if (!local.internal && !remoteExists) {
+ await this.remove(local.id);
+ }
+ })
+ );
}
async updateState(subscriptionId, state) {
@@ -108,9 +105,12 @@ class SubscriptionManager {
return false;
}
try {
- // eslint-disable-next-line no-param-reassign
- notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
- await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
+ await db.notifications.add({
+ ...notification,
+ subscriptionId,
+ // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
+ new: 1,
+ }); // FIXME consider put() for double tab
await db.subscriptions.update(subscriptionId, {
last: notification.id,
});
diff --git a/web/src/app/utils.js b/web/src/app/utils.js
index 0af1033..ab7551b 100644
--- a/web/src/app/utils.js
+++ b/web/src/app/utils.js
@@ -118,10 +118,10 @@ export const maybeWithBearerAuth = (headers, token) => {
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
export const maybeWithAuth = (headers, user) => {
- if (user && user.password) {
+ if (user?.password) {
return withBasicAuth(headers, user.username, user.password);
}
- if (user && user.token) {
+ if (user?.token) {
return withBearerAuth(headers, user.token);
}
return headers;
@@ -139,17 +139,14 @@ export const maybeAppendActionErrors = (message, notification) => {
};
export const shuffle = (arr) => {
- let j;
- let x;
- for (let index = arr.length - 1; index > 0; index -= 1) {
- j = Math.floor(Math.random() * (index + 1));
- x = arr[index];
- // eslint-disable-next-line no-param-reassign
- arr[index] = arr[j];
- // eslint-disable-next-line no-param-reassign
- arr[j] = x;
+ const returnArr = [...arr];
+
+ for (let index = returnArr.length - 1; index > 0; index -= 1) {
+ const j = Math.floor(Math.random() * (index + 1));
+ [returnArr[index], returnArr[j]] = [returnArr[j], returnArr[index]];
}
- return arr;
+
+ return returnArr;
};
export const splitNoEmpty = (s, delimiter) =>
diff --git a/web/src/components/EmojiPicker.jsx b/web/src/components/EmojiPicker.jsx
index f9e8b5e..d1fb170 100644
--- a/web/src/components/EmojiPicker.jsx
+++ b/web/src/components/EmojiPicker.jsx
@@ -127,17 +127,7 @@ const Category = (props) => {
);
};
-const emojiMatches = (emoji, words) => {
- if (words.length === 0) {
- return true;
- }
- for (const word of words) {
- if (emoji.searchBase.indexOf(word) === -1) {
- return false;
- }
- }
- return true;
-};
+const emojiMatches = (emoji, words) => words.length === 0 || words.some((word) => emoji.searchBase.includes(word));
const Emoji = (props) => {
const { emoji } = props;
diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx
index 7d4da06..2faf2fd 100644
--- a/web/src/components/Notifications.jsx
+++ b/web/src/components/Notifications.jsx
@@ -436,15 +436,10 @@ const ACTION_LABEL_SUFFIX = {
};
const updateActionStatus = (notification, action, progress, error) => {
- // TODO(eslint): Fix by spreading? Does the code depend on the change, though?
- // eslint-disable-next-line no-param-reassign
- notification.actions = notification.actions.map((a) => {
- if (a.id !== action.id) {
- return a;
- }
- return { ...a, progress, error };
+ subscriptionManager.updateNotification({
+ ...notification,
+ actions: notification.actions.map((a) => (a.id === action.id ? { ...a, progress, error } : a)),
});
- subscriptionManager.updateNotification(notification);
};
const performHttpAction = async (notification, action) => {
diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js
index b9c5536..6b68188 100644
--- a/web/src/components/hooks.js
+++ b/web/src/components/hooks.js
@@ -47,6 +47,13 @@ export const useConnectionListeners = (account, subscriptions, users) => {
const handleMessage = async (subscriptionId, message) => {
const subscription = await subscriptionManager.get(subscriptionId);
+
+ // Race condition: sometimes the subscription is already unsubscribed from account
+ // sync before the message is handled
+ if (!subscription) {
+ return;
+ }
+
if (subscription.internal) {
await handleInternalMessage(message);
} else {