diff --git a/cmd/app.go b/cmd/app.go
index 50f80564..379a58a0 100644
--- a/cmd/app.go
+++ b/cmd/app.go
@@ -74,7 +74,7 @@ func execRun(c *cli.Context) error {
return errors.New("keepalive interval cannot be lower than five seconds")
} else if managerInterval < 5*time.Second {
return errors.New("manager interval cannot be lower than five seconds")
- } else if cacheDuration < managerInterval {
+ } else if cacheDuration > 0 && cacheDuration < managerInterval {
return errors.New("cache duration cannot be lower than manager interval")
} else if keyFile != "" && !util.FileExists(keyFile) {
return errors.New("if set, key file must exist")
diff --git a/config/config.yml b/config/config.yml
index dec13fbb..972b4f94 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -28,6 +28,8 @@
# If set, messages are cached in a local SQLite database instead of only in-memory. This
# allows for service restarts without losing messages in support of the since= parameter.
#
+# To disable the cache entirely (on-disk/in-memory), set "cache-duration" to 0.
+#
# Note: If you are running ntfy with systemd, make sure this cache file is owned by the
# ntfy user and group by running: chown ntfy.ntfy
curl \
@@ -203,7 +203,7 @@
Click the link to do so.
- Recent notifications (cached for {{.CacheDuration}}):
+ Recent notifications ({{if .CacheDuration}}cached for {{.CacheDuration | durationToHuman}}{{else}}caching is disabled{{end}}):
You haven't received any notifications for this topic yet.
diff --git a/server/server.go b/server/server.go
index 0edaab98..8b6c364d 100644
--- a/server/server.go
+++ b/server/server.go
@@ -49,7 +49,7 @@ func (e errHTTP) Error() string {
type indexPage struct {
Topic string
- CacheDuration string
+ CacheDuration time.Duration
}
type sinceTime time.Time
@@ -85,9 +85,13 @@ var (
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
disallowedTopics = []string{"docs", "static"}
+ templateFnMap = template.FuncMap{
+ "durationToHuman": util.DurationToHuman,
+ }
+
//go:embed "index.gohtml"
indexSource string
- indexTemplate = template.Must(template.New("index").Parse(indexSource))
+ indexTemplate = template.Must(template.New("index").Funcs(templateFnMap).Parse(indexSource))
//go:embed "example.html"
exampleSource string
@@ -139,7 +143,9 @@ func New(conf *config.Config) (*Server, error) {
}
func createCache(conf *config.Config) (cache, error) {
- if conf.CacheFile != "" {
+ if conf.CacheDuration == 0 {
+ return newNopCache(), nil
+ } else if conf.CacheFile != "" {
return newSqliteCache(conf.CacheFile)
}
return newMemCache(), nil
@@ -241,7 +247,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
return indexTemplate.Execute(w, &indexPage{
Topic: r.URL.Path[1:],
- CacheDuration: util.DurationToHuman(s.config.CacheDuration),
+ CacheDuration: s.config.CacheDuration,
})
}
@@ -278,15 +284,17 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
if m.Message == "" {
return errHTTPBadRequest
}
- title, priority, tags := parseHeaders(r.Header)
+ title, priority, tags, cache := parseHeaders(r.Header)
m.Title = title
m.Priority = priority
m.Tags = tags
if err := t.Publish(m); err != nil {
return err
}
- if err := s.cache.AddMessage(m); err != nil {
- return err
+ if cache {
+ if err := s.cache.AddMessage(m); err != nil {
+ return err
+ }
}
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
if err := json.NewEncoder(w).Encode(m); err != nil {
@@ -298,7 +306,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
return nil
}
-func parseHeaders(header http.Header) (title string, priority int, tags []string) {
+func parseHeaders(header http.Header) (title string, priority int, tags []string, cache bool) {
title = readHeader(header, "x-title", "title", "ti", "t")
priorityStr := readHeader(header, "x-priority", "priority", "prio", "p")
if priorityStr != "" {
@@ -324,7 +332,8 @@ func parseHeaders(header http.Header) (title string, priority int, tags []string
tags = append(tags, strings.TrimSpace(s))
}
}
- return title, priority, tags
+ cache = readHeader(header, "x-cache", "cache") != "no"
+ return title, priority, tags, cache
}
func readHeader(header http.Header, names ...string) string {
diff --git a/server/server_test.go b/server/server_test.go
index e4e9448f..1513cb90 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -150,7 +150,74 @@ func TestServer_StaticSites(t *testing.T) {
assert.Equal(t, 200, rr.Code)
assert.Contains(t, rr.Body.String(), `Made with ❤️ by Philipp C. Heckel`)
assert.Contains(t, rr.Body.String(), ``)
+}
+func TestServer_PublishNoCache(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ response := request(t, s, "PUT", "/mytopic", "this message is not cached", map[string]string{
+ "Cache": "no",
+ })
+ msg := toMessage(t, response.Body.String())
+ assert.NotEmpty(t, msg.ID)
+ assert.Equal(t, "this message is not cached", msg.Message)
+
+ response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
+ messages := toMessages(t, response.Body.String())
+ assert.Empty(t, messages)
+}
+
+func TestServer_PublishAndMultiPoll(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ response := request(t, s, "PUT", "/mytopic1", "message 1", nil)
+ msg := toMessage(t, response.Body.String())
+ assert.NotEmpty(t, msg.ID)
+ assert.Equal(t, "mytopic1", msg.Topic)
+ assert.Equal(t, "message 1", msg.Message)
+
+ response = request(t, s, "PUT", "/mytopic2", "message 2", nil)
+ msg = toMessage(t, response.Body.String())
+ assert.NotEmpty(t, msg.ID)
+ assert.Equal(t, "mytopic2", msg.Topic)
+ assert.Equal(t, "message 2", msg.Message)
+
+ response = request(t, s, "GET", "/mytopic1/json?poll=1", "", nil)
+ messages := toMessages(t, response.Body.String())
+ assert.Equal(t, 1, len(messages))
+ assert.Equal(t, "mytopic1", messages[0].Topic)
+ assert.Equal(t, "message 1", messages[0].Message)
+
+ response = request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1", "", nil)
+ messages = toMessages(t, response.Body.String())
+ assert.Equal(t, 2, len(messages))
+ assert.Equal(t, "mytopic1", messages[0].Topic)
+ assert.Equal(t, "message 1", messages[0].Message)
+ assert.Equal(t, "mytopic2", messages[1].Topic)
+ assert.Equal(t, "message 2", messages[1].Message)
+}
+
+func TestServer_PublishWithNopCache(t *testing.T) {
+ c := newTestConfig(t)
+ c.CacheDuration = 0
+ s := newTestServer(t, c)
+
+ subscribeRR := httptest.NewRecorder()
+ subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR)
+
+ publishRR := request(t, s, "PUT", "/mytopic", "my first message", nil)
+ assert.Equal(t, 200, publishRR.Code)
+
+ subscribeCancel()
+ messages := toMessages(t, subscribeRR.Body.String())
+ assert.Equal(t, 2, len(messages))
+ assert.Equal(t, openEvent, messages[0].Event)
+ assert.Equal(t, messageEvent, messages[1].Event)
+ assert.Equal(t, "my first message", messages[1].Message)
+
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
+ messages = toMessages(t, response.Body.String())
+ assert.Empty(t, messages)
}
func newTestConfig(t *testing.T) *config.Config {
diff --git a/server/static/css/app.css b/server/static/css/app.css
index 07dcc75b..a339d490 100644
--- a/server/static/css/app.css
+++ b/server/static/css/app.css
@@ -470,11 +470,19 @@ li {
margin-bottom: 20px;
}
+#detail .detailDate {
+ margin-bottom: 2px;
+}
+
#detail .detailDate, #detail .detailTags {
color: #888;
font-size: 0.9em;
}
+#detail .detailTags {
+ margin-top: 2px;
+}
+
#detail .detailDate img {
width: 20px;
height: 20px;
@@ -483,11 +491,6 @@ li {
#detail .detailTitle {
font-weight: bold;
- font-size: 1.1em;
-}
-
-#detail .detailMessage {
- font-size: 1.1em;
}
#detail #detailMain {
diff --git a/server/static/js/app.js b/server/static/js/app.js
index b1cf4401..c8f47a5e 100644
--- a/server/static/js/app.js
+++ b/server/static/js/app.js
@@ -294,7 +294,7 @@ const formatTitle = (m) => {
const formatTitleA = (m) => {
const emojiList = toEmojis(m.tags);
- if (emojiList) {
+ if (emojiList.length > 0) {
return `${emojiList.join(" ")} ${m.title}`;
} else {
return m.title;
@@ -306,7 +306,7 @@ const formatMessage = (m) => {
return m.message;
} else {
const emojiList = toEmojis(m.tags);
- if (emojiList) {
+ if (emojiList.length > 0) {
return `${emojiList.join(" ")} ${m.message}`;
} else {
return m.message;