Compare commits

...

15 Commits

Author SHA1 Message Date
Philipp Heckel
a327fe802e Make web app work for updated notifications 2022-03-25 11:01:07 -04:00
Philipp Heckel
86baa80ab8 Merge branch 'main' into update-messages 2022-03-24 19:30:27 -04:00
Philipp Heckel
5a99fe8ba2 Try it a better way 2022-03-24 19:27:07 -04:00
Philipp Heckel
ee0f448d86 Another try 2022-03-24 19:25:41 -04:00
Philipp Heckel
a222f64ee4 Hopefully fix the build 2022-03-24 19:23:30 -04:00
Philipp Heckel
0a77c5296b Add since=$ID/$timestamp parsing logic 2022-03-24 17:05:07 -04:00
Philipp Heckel
b7871b80ab Merge branch 'main' into update-messages 2022-03-24 13:27:04 -04:00
Philipp Heckel
140daec0d3 Fix date formatting issue in example 2022-03-24 13:17:04 -04:00
Philipp Heckel
8939173a1e Continued work 2022-03-23 21:51:38 -04:00
Philipp Heckel
8848829dfa WIP: Update messages 2022-03-23 16:39:22 -04:00
Philipp Heckel
b409c89d3b Do not allow comma in topic name in publish via GET endpoint (no ticket) 2022-03-23 14:29:55 -04:00
Philipp Heckel
806893962c Changelog 2022-03-23 11:02:12 -04:00
Philipp Heckel
14d3c5e93e Changelog 2022-03-22 20:31:43 -04:00
Philipp Heckel
37e14b13a4 Update deps 2022-03-22 19:54:20 -04:00
Philipp Heckel
d7fa51be2c Changelog 2022-03-22 15:26:15 -04:00
15 changed files with 894 additions and 566 deletions

View File

@@ -9,10 +9,24 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Features:**
* Download attachments to cache folder ([#181](https://github.com/binwiederhier/ntfy/issues/181))
* Regularly delete attachments for deleted notifications ([#142](https://github.com/binwiederhier/ntfy/issues/142))
Bugs:
**Bugs:**
* IllegalStateException: Failed to build unique file ([#177](https://github.com/binwiederhier/ntfy/issues/177), thanks to [@Fallenbagel](https://github.com/Fallenbagel) for reporting)
* SQLiteConstraintException: Crash during UP registration ([#185](https://github.com/binwiederhier/ntfy/issues/185))
* Refresh preferences screen after settings import (#183, thanks to [@cmeis](https://github.com/cmeis) for reporting)
**Thanks:**
* Many thanks to [@cmeis](https://github.com/cmeis), [@Fallenbagel](https://github.com/Fallenbagel), [@Joeharrison94](https://github.com/Joeharrison94),
and [@rogeliodh](https://github.com/rogeliodh) for input on the new attachment logic, and for testing the release
## ntfy server v1.19.0 (UNRELEASED)
**Bugs:**
* Do not allow comma in topic name in publish via GET endpoint (no ticket)
-->

10
go.mod
View File

@@ -15,7 +15,7 @@ require (
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.4.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
@@ -28,7 +28,7 @@ require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/AlekSi/pointer v1.0.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -40,12 +40,12 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect
google.golang.org/genproto v0.0.0-20220322021311-435b647f9ef2 // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

17
go.sum
View File

@@ -61,8 +61,9 @@ cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKu
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE=
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
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.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
@@ -244,8 +245,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -317,6 +319,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -406,8 +409,9 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -604,8 +608,8 @@ google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE=
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220322021311-435b647f9ef2 h1:3n0D2NdPGm0g0wrVJzXJWW5CBOoqgGBkDX9cRMJHZAY=
google.golang.org/genproto v0.0.0-20220322021311-435b647f9ef2/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -648,8 +652,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.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-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,9 +1,3 @@
# The documentation uses 'mkdocs', which is written in Python
# See https://github.com/squidfunk/mkdocs-material/issues/2030
jinja2>=2.11.1
# mkdocs
mkdocs
mkdocs-material
mkdocs-minify-plugin

View File

@@ -40,14 +40,16 @@ var (
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
errHTTPBadRequestWebSocketsUpgradeHeaderMissing = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets"}
errHTTPBadRequestJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "not found", ""}
errHTTPNotFoundMessageID = &errHTTP{40402, http.StatusNotFound, "not found: unable to find message with this ID", "https://ntfy.sh/docs/publish/#updating-messages"} // FIXME LINK
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth limit reached, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsUpdatingTooQuickly = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many consecutive message updates", "https://ntfy.sh/docs/publish/#updating-messages"} // FIXME LINK
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
)

View File

@@ -23,6 +23,7 @@ const (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mid TEXT NOT NULL,
time INT NOT NULL,
updated INT NOT NULL,
topic TEXT NOT NULL,
message TEXT NOT NULL,
title TEXT NOT NULL,
@@ -43,41 +44,47 @@ const (
COMMIT;
`
insertMessageQuery = `
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO messages (mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
updateMessageQuery = `UPDATE messages SET updated = ?, message = ?, title = ?, priority = ?, tags = ?, click = ? WHERE topic = ? AND mid = ?`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?`
selectMessagesSinceTimeQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND id > ? AND published = 1
WHERE topic = ? AND id >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
WHERE topic = ? AND (id >= ? OR published = 0)
ORDER BY time, id
`
selectMessagesDueQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE time <= ? AND published = 0
ORDER BY time, id
`
selectMessageByIDQuery = `
SELECT mid, time, updated, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND mid = ?
`
updateMessagePublishedQuery = `UPDATE messages SET published = 1 WHERE mid = ?`
selectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
selectMessageCountForTopicQuery = `SELECT COUNT(*) FROM messages WHERE topic = ?`
@@ -88,7 +95,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 5
currentSchemaVersion = 6
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@@ -166,6 +173,11 @@ const (
ALTER TABLE messages_new RENAME TO messages;
COMMIT;
`
// 5 -> 6
migrate5To6AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN updated INT NOT NULL DEFAULT (0);
`
)
type messageCache struct {
@@ -232,6 +244,7 @@ func (c *messageCache) AddMessage(m *message) error {
insertMessageQuery,
m.ID,
m.Time,
m.Updated,
m.Topic,
m.Message,
m.Title,
@@ -250,6 +263,28 @@ func (c *messageCache) AddMessage(m *message) error {
return err
}
func (c *messageCache) UpdateMessage(m *message) error {
if m.Event != messageEvent {
return errUnexpectedMessageType
}
if c.nop {
return nil
}
tags := strings.Join(m.Tags, ",")
_, err := c.db.Exec(
updateMessageQuery,
m.Updated,
m.Message,
m.Title,
m.Priority,
tags,
m.Click,
m.Topic,
m.ID,
)
return err
}
func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
if since.IsNone() {
return make([]*message, 0), nil
@@ -296,7 +331,15 @@ func (c *messageCache) messagesSinceID(topic string, since sinceMarker, schedule
if err != nil {
return nil, err
}
return readMessages(rows)
messages, err := readMessages(rows)
if err != nil {
return nil, err
} else if len(messages) == 0 {
return messages, nil
} else if since.IsTime() && messages[0].Updated > since.Time().Unix() {
return messages, nil
}
return messages[1:], nil // Do not include row with ID itself
}
func (c *messageCache) MessagesDue() ([]*message, error) {
@@ -393,16 +436,31 @@ func (c *messageCache) AttachmentsExpired() ([]string, error) {
return ids, nil
}
func (c *messageCache) Message(topic, id string) (*message, error) {
rows, err := c.db.Query(selectMessageByIDQuery, topic, id)
if err != nil {
return nil, err
}
messages, err := readMessages(rows)
if err != nil {
return nil, err
} else if len(messages) == 0 {
return nil, errors.New("not found")
}
return messages[0], nil
}
func readMessages(rows *sql.Rows) ([]*message, error) {
defer rows.Close()
messages := make([]*message, 0)
for rows.Next() {
var timestamp, attachmentSize, attachmentExpires int64
var timestamp, updated, attachmentSize, attachmentExpires int64
var priority int
var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string
err := rows.Scan(
&id,
&timestamp,
&updated,
&topic,
&msg,
&title,
@@ -438,6 +496,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
messages = append(messages, &message{
ID: id,
Time: timestamp,
Updated: updated,
Event: messageEvent,
Topic: topic,
Message: msg,
@@ -490,6 +549,8 @@ func setupCacheDB(db *sql.DB) error {
return migrateFrom3(db)
} else if schemaVersion == 4 {
return migrateFrom4(db)
} else if schemaVersion == 5 {
return migrateFrom5(db)
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
@@ -562,5 +623,16 @@ func migrateFrom4(db *sql.DB) error {
if _, err := db.Exec(updateSchemaVersion, 5); err != nil {
return err
}
return migrateFrom5(db)
}
func migrateFrom5(db *sql.DB) error {
log.Print("Migrating cache database schema: from 5 to 6")
if _, err := db.Exec(migrate5To6AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(updateSchemaVersion, 6); err != nil {
return err
}
return nil // Update this when a new version is added
}

View File

@@ -55,15 +55,16 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
var (
// If changed, don't forget to update Android App and auth_sqlite.go
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
extTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
updateTopicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[A-Za-z0-9]{12}$`) // ID length must match messageIDLength & util.randomStringCharset
externalTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
authPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
webConfigPath = "/config.js"
staticRegex = regexp.MustCompile(`^/static/.+`)
@@ -279,7 +280,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.handleOptions(w, r)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updateTopicPathRegex.MatchString(r.URL.Path)) {
return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
@@ -293,7 +294,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || extTopicPathRegex.MatchString(r.URL.Path)) {
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
return s.handleTopic(w, r)
}
return errHTTPNotFound
@@ -390,7 +391,26 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
}
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t, err := s.topicFromPath(r.URL.Path)
t, messageID, err := s.topicAndMessageIDFromPath(r.URL.Path)
if err != nil {
return err
}
var m *message
update := messageID != ""
if update {
m, err = s.messageCache.Message(t.ID, messageID)
if err != nil {
return errHTTPNotFoundMessageID
}
newUpdated := time.Now().Unix()
if newUpdated <= m.Updated {
return errHTTPTooManyRequestsUpdatingTooQuickly
}
m.Updated = newUpdated
} else {
m = newDefaultMessage(t.ID, "")
}
cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
if err != nil {
return err
}
@@ -398,11 +418,6 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
if err != nil {
return err
}
m := newDefaultMessage(t.ID, "")
cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
if err != nil {
return err
}
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
return err
}
@@ -430,8 +445,14 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
}()
}
if cache {
if err := s.messageCache.AddMessage(m); err != nil {
return err
if update {
if err := s.messageCache.UpdateMessage(m); err != nil {
return err
}
} else {
if err := s.messageCache.AddMessage(m); err != nil {
return err
}
}
}
w.Header().Set("Content-Type", "application/json")
@@ -447,9 +468,19 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
cache = readBoolParam(r, true, "x-cache", "cache")
if !cache && m.Updated != 0 {
return false, false, "", false, errors.New("message updates must be cached")
}
// TODO more restrictions
firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t")
m.Click = readParam(r, "x-click", "click")
title := readParam(r, "x-title", "title", "t")
if title != "" {
m.Title = title
}
click := readParam(r, "x-click", "click")
if click != "" {
m.Click = click
}
filename := readParam(r, "x-filename", "filename", "file", "f")
attach := readParam(r, "x-attach", "attach", "a")
if attach != "" || filename != "" {
@@ -489,9 +520,11 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
if messageStr != "" {
m.Message = messageStr
}
m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
priority, err := util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if err != nil {
return false, false, "", false, errHTTPBadRequestPriorityInvalid
} else if priority > 0 {
m.Priority = priority
}
tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
if tagsStr != "" {
@@ -870,6 +903,13 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
return sinceNoMessages, nil
}
// ID/timestamp
parts := strings.Split(since, "/")
if len(parts) == 2 && validMessageID(parts[0]) && validUnixTimestamp(parts[1]) {
t, _ := toUnixTimestamp(parts[1])
return newSince(parts[0], t), nil
}
// ID, timestamp, duration
if validMessageID(since) {
return newSinceID(since), nil
@@ -888,16 +928,20 @@ func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
return nil
}
func (s *Server) topicFromPath(path string) (*topic, error) {
func (s *Server) topicAndMessageIDFromPath(path string) (*topic, string, error) {
parts := strings.Split(path, "/")
if len(parts) < 2 {
return nil, errHTTPBadRequestTopicInvalid
if len(parts) != 2 && len(parts) != 3 {
return nil, "", errHTTPBadRequestTopicInvalid
}
topics, err := s.topicsFromIDs(parts[1])
if err != nil {
return nil, err
return nil, "", err
}
return topics[0], nil
messageID := ""
if len(parts) == 3 && len(parts[2]) == messageIDLength {
messageID = parts[2]
}
return topics[0], messageID, nil
}
func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {

View File

@@ -72,6 +72,7 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
data = map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"updated": fmt.Sprintf("%d", m.Updated),
"event": m.Event,
"topic": m.Topic,
"priority": fmt.Sprintf("%d", m.Priority),

View File

@@ -77,6 +77,7 @@ func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
require.Equal(t, map[string]string{
"id": m.ID,
"time": fmt.Sprintf("%d", m.Time),
"updated": "0",
"event": "message",
"topic": "mytopic",
"priority": "4",

View File

@@ -203,6 +203,14 @@ func TestServer_PublishPriority(t *testing.T) {
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
}
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "GET", "/mytopic,mytopic2/publish?m=hi", "", nil)
require.Equal(t, 404, response.Code)
}
func TestServer_PublishNoCache(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
@@ -382,6 +390,69 @@ func TestServer_PublishAndPollSince(t *testing.T) {
require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code)
}
func TestServer_PublishUpdateAndPollSince(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
// Initial PUT
response := request(t, s, "PUT", "/mytopic?t=atitle&tags=tag1,tag2&prio=high&click=https://google.com&attach=https://heckel.io", "test 1", nil)
message1 := toMessage(t, response.Body.String())
require.Equal(t, int64(0), message1.Updated)
require.Equal(t, "test 1", message1.Message)
require.Equal(t, "atitle", message1.Title)
require.Equal(t, 4, message1.Priority)
require.Equal(t, []string{"tag1", "tag2"}, message1.Tags)
require.Equal(t, "https://google.com", message1.Click)
require.Equal(t, "https://heckel.io", message1.Attachment.URL)
// Update
response = request(t, s, "PUT", "/mytopic/"+message1.ID+"?prio=low", "test 2", nil)
message2 := toMessage(t, response.Body.String())
require.Equal(t, message1.ID, message2.ID)
require.True(t, message2.Updated > message1.Updated)
require.Equal(t, "test 2", message2.Message) // Updated
require.Equal(t, "atitle", message2.Title)
require.Equal(t, 2, message2.Priority) // Updated
require.Equal(t, []string{"tag1", "tag2"}, message2.Tags)
require.Equal(t, "https://google.com", message2.Click)
require.Equal(t, "https://heckel.io", message2.Attachment.URL)
time.Sleep(1100 * time.Millisecond)
// Another update
response = request(t, s, "PUT", "/mytopic/"+message1.ID+"?title=new+title", "test 3", nil)
message3 := toMessage(t, response.Body.String())
require.True(t, message3.Updated > message2.Updated)
require.Equal(t, "test 3", message3.Message) // Updated
require.Equal(t, "new title", message3.Title) // Updated
// Get all messages: Should be only one that was updated
since := "all"
response = request(t, s, "GET", "/mytopic/json?since="+since+"&poll=1", "", nil)
messages := toMessages(t, response.Body.String())
require.Equal(t, 1, len(messages))
require.Equal(t, message1.ID, messages[0].ID)
require.Equal(t, "test 3", messages[0].Message)
// Get all messages since "message ID": Should be zero, since we know this message
since = message1.ID
response = request(t, s, "GET", "/mytopic/json?since="+since+"&poll=1", "", nil)
messages = toMessages(t, response.Body.String())
require.Equal(t, 0, len(messages))
// Get all messages since "message ID" but with an older timestamp: Should be the latest updated message
since = fmt.Sprintf("%s/%d", message1.ID, message2.Updated) // We're missing an update
response = request(t, s, "GET", "/mytopic/json?since="+since+"&poll=1", "", nil)
messages = toMessages(t, response.Body.String())
require.Equal(t, 1, len(messages))
require.Equal(t, "test 3", messages[0].Message)
// Get all messages since "message ID" with the current timestamp: No messages expected
since = fmt.Sprintf("%s/%d", message3.ID, message3.Updated) // We are up-to-date
response = request(t, s, "GET", "/mytopic/json?since="+since+"&poll=1", "", nil)
messages = toMessages(t, response.Body.String())
require.Equal(t, 0, len(messages))
}
func TestServer_PublishViaGET(t *testing.T) {
s := newTestServer(t, newTestConfig(t))

View File

@@ -1,8 +1,10 @@
package server
import (
"errors"
"heckel.io/ntfy/util"
"net/http"
"strconv"
"time"
)
@@ -20,9 +22,11 @@ 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
Event string `json:"event"` // One of the above
ID string `json:"id"` // Random message ID
Time int64 `json:"time"` // Unix time in seconds
Updated int64 `json:"updated,omitempty"` // Set if updated, unix time in seconds
Deleted int64 `json:"deleted,omitempty"` // Set if deleted, unix time in seconds
Event string `json:"event"` // One of the above
Topic string `json:"topic"`
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
@@ -30,7 +34,7 @@ type message struct {
Attachment *attachment `json:"attachment,omitempty"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes
}
type attachment struct {
@@ -90,11 +94,31 @@ func validMessageID(s string) bool {
return util.ValidRandomString(s, messageIDLength)
}
func validUnixTimestamp(s string) bool {
_, err := toUnixTimestamp(s)
return err == nil
}
func toUnixTimestamp(s string) (int64, error) {
u, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, err
}
if u < 1000000000 || u > 3000000000 { // I know. It's practical. So relax ...
return 0, errors.New("invalid unix date")
}
return u, nil
}
type sinceMarker struct {
time time.Time
id string
}
func newSince(id string, timestamp int64) sinceMarker {
return sinceMarker{time.Unix(timestamp, 0), id}
}
func newSinceTime(timestamp int64) sinceMarker {
return sinceMarker{time.Unix(timestamp, 0), ""}
}
@@ -115,6 +139,10 @@ func (t sinceMarker) IsID() bool {
return t.id != ""
}
func (t sinceMarker) IsTime() bool {
return t.time.Unix() > 0
}
func (t sinceMarker) Time() time.Time {
return t.time
}

View File

@@ -17,7 +17,7 @@ import (
)
const (
randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // Update updateTopicPathRegex if changed
)
var (

1079
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,13 +65,17 @@ class SubscriptionManager {
/** Adds notification, or returns false if it already exists */
async addNotification(subscriptionId, notification) {
const exists = await db.notifications.get(notification.id);
if (exists) {
return false;
const existingNotification = await db.notifications.get(notification.id);
if (existingNotification) {
const upToDate = (existingNotification?.updated ?? 0) >= (notification.updated ?? 0);
if (upToDate) {
console.error(`[SubscriptionManager] up to date`, existingNotification, notification);
return false;
}
}
try {
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.put({ ...notification, subscriptionId });
await db.subscriptions.update(subscriptionId, {
last: notification.id
});

View File

@@ -121,13 +121,16 @@ const SettingsIcons = (props) => {
"Titles are optional, did you know that?",
"ntfy is open source, and will always be free. Cool, right?",
"I don't really like apples",
"My favorite TV show is The Wire. You should watch it!"
"My favorite TV show is The Wire. You should watch it!",
"You can attach files and URLs to messages too",
"You can delay messages up to 3 days"
])[0];
const nowSeconds = Math.round(Date.now()/1000);
const message = shuffle([
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(Date.now())} right now. Is that early or late?`,
`Hello friend, this is a test notification from ntfy web. It's ${formatShortDateTime(nowSeconds)} right now. Is that early or late?`,
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
`Alright then, it's ${formatShortDateTime(Date.now())} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
`Alright then, it's ${formatShortDateTime(nowSeconds)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
`It's interesting to hear what people use ntfy for. I've heard people talk about using it for so many cool things. What do you use it for?`