Compare commits

...

14 Commits

Author SHA1 Message Date
Philipp Heckel
96a12d98c9 WIP: Templating 2022-03-15 11:39:51 -04:00
Philipp Heckel
53375ff559 Examples 2022-03-15 08:27:17 -04:00
Philipp Heckel
53e08988e7 rpm: Do not replace server.yml, closes #166 2022-03-14 17:21:28 -04:00
Philipp Heckel
d0bbda555f Add Android WebSockets deprecation, remove 'ntfy serve' deprecation 2022-03-13 22:16:48 -04:00
Philipp Heckel
207e990798 Fix brittle test 2022-03-13 21:30:14 -04:00
Philipp Heckel
b0a07af28d Changelog 2022-03-13 20:21:43 -04:00
Philipp C. Heckel
1a8bac7ab1 Update README.md 2022-03-13 16:08:38 -04:00
Philipp Heckel
dc03c13642 Update docs for UnifiedPush 2.0 spec 2022-03-13 16:06:40 -04:00
Philipp C. Heckel
739b20583d Update releases.md 2022-03-12 10:33:23 -05:00
Philipp Heckel
10ccbc780b Docs, bump version 2022-03-12 08:37:23 -05:00
Philipp Heckel
f971a36ec0 Merge branch 'main' of github.com:binwiederhier/ntfy into main 2022-03-12 08:15:48 -05:00
Philipp Heckel
3699464947 Remove crypto.subtle requirement 2022-03-12 08:15:30 -05:00
Philipp C. Heckel
3a3d1262ab Merge pull request #156 from ChaseCares/ChaseCares-readme-screenshots
Update README.md
2022-03-11 22:07:33 -05:00
ChaseCares
395a97c0e5 Update README.md
Commit 4a6aca4 changed the directory structure, this pull requests updates screenshot URLs.

Feel free to disregard, I am new to submitting pull requests.
Looks great!
Chase
2022-03-11 19:02:44 -08:00
19 changed files with 208 additions and 84 deletions

View File

@@ -59,12 +59,12 @@ nfpms:
contents: contents:
- src: server/server.yml - src: server/server.yml
dst: /etc/ntfy/server.yml dst: /etc/ntfy/server.yml
type: config type: "config|noreplace"
- src: server/ntfy.service - src: server/ntfy.service
dst: /lib/systemd/system/ntfy.service dst: /lib/systemd/system/ntfy.service
- src: client/client.yml - src: client/client.yml
dst: /etc/ntfy/client.yml dst: /etc/ntfy/client.yml
type: config type: "config|noreplace"
- src: client/ntfy-client.service - src: client/ntfy-client.service
dst: /lib/systemd/system/ntfy-client.service dst: /lib/systemd/system/ntfy-client.service
- dst: /var/cache/ntfy - dst: /var/cache/ntfy

View File

@@ -149,6 +149,14 @@ release-check-tags:
echo "ERROR: Must update docs/install.md with latest tag first.";\ echo "ERROR: Must update docs/install.md with latest tag first.";\
exit 1;\ exit 1;\
fi fi
if grep -q XXXXX docs/releases.md; then\
echo "ERROR: Must update docs/releases.md, found XXXXX.";\
exit 1;\
fi
if ! grep -q $(LATEST_TAG) docs/releases.md; then\
echo "ERROR: Must update docs/releases.mdwith latest tag first.";\
exit 1;\
fi
release: build-deps release-check-tags check release: build-deps release-check-tags check
goreleaser release --rm-dist --debug goreleaser release --rm-dist --debug

View File

@@ -1,4 +1,4 @@
![ntfy](server/static/img/ntfy.png) ![ntfy](web/public/static/img/ntfy.png)
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST # 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) [![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
@@ -18,11 +18,11 @@ I run a free version of it at **[ntfy.sh](https://ntfy.sh)**, and there's an [op
too. too.
<p> <p>
<img src="server/static/img/screenshot-curl.png" height="180"> <img src="web/public/static/img/screenshot-curl.png" height="180">
<img src="server/static/img/screenshot-web-detail.png" height="180"> <img src="web/public/static/img/screenshot-web-detail.png" height="180">
<img src="server/static/img/screenshot-phone-main.jpg" height="180"> <img src="web/public/static/img/screenshot-phone-main.jpg" height="180">
<img src="server/static/img/screenshot-phone-detail.jpg" height="180"> <img src="web/public/static/img/screenshot-phone-detail.jpg" height="180">
<img src="server/static/img/screenshot-phone-notification.jpg" height="180"> <img src="web/public/static/img/screenshot-phone-notification.jpg" height="180">
</p> </p>
## **[Documentation](https://ntfy.sh/docs/)** ## **[Documentation](https://ntfy.sh/docs/)**

View File

@@ -30,9 +30,6 @@ func New() *cli.App {
Reader: os.Stdin, Reader: os.Stdin,
Writer: os.Stdout, Writer: os.Stdout,
ErrWriter: os.Stderr, ErrWriter: os.Stderr,
Action: execMainApp,
Before: initConfigFileInputSource("config", flagsServe), // DEPRECATED, see deprecation notice
Flags: flagsServe, // DEPRECATED, see deprecation notice
Commands: []*cli.Command{ Commands: []*cli.Command{
// Server commands // Server commands
cmdServe, cmdServe,
@@ -46,12 +43,6 @@ func New() *cli.App {
} }
} }
func execMainApp(c *cli.Context) error {
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mDeprecation notice: Please run the server using 'ntfy serve'; see 'ntfy -h' for help.\x1b[0m")
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mThis way of running the server will be removed March 2022. See https://ntfy.sh/docs/deprecations/ for details.\x1b[0m")
return execServe(c)
}
// initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks // initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks
// if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails. // if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails.
func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFunc { func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFunc {

View File

@@ -34,6 +34,7 @@ var flagsServe = []cli.Flag{
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}), altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"M"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, DefaultText: "4K", Usage: "size limit of messages before they are treated as attachments (e.g. 4K, 64K)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -95,6 +96,7 @@ func execServe(c *cli.Context) error {
keepaliveInterval := c.Duration("keepalive-interval") keepaliveInterval := c.Duration("keepalive-interval")
managerInterval := c.Duration("manager-interval") managerInterval := c.Duration("manager-interval")
webRoot := c.String("web-root") webRoot := c.String("web-root")
messageSizeLimitStr := c.String("message-size-limit")
smtpSenderAddr := c.String("smtp-sender-addr") smtpSenderAddr := c.String("smtp-sender-addr")
smtpSenderUser := c.String("smtp-sender-user") smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass") smtpSenderPass := c.String("smtp-sender-pass")
@@ -171,6 +173,12 @@ func execServe(c *cli.Context) error {
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt { } else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt) return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
} }
messageSizeLimit, err := parseSize(messageSizeLimitStr, server.DefaultMessageLengthLimit)
if err != nil {
return err
} else if messageSizeLimit > server.MaxMessageLengthLimit {
return fmt.Errorf("config option message-size-limit must be lower than %d", server.MaxMessageLengthLimit)
}
// Resolve hosts // Resolve hosts
visitorRequestLimitExemptIPs := make([]string, 0) visitorRequestLimitExemptIPs := make([]string, 0)
@@ -206,6 +214,7 @@ func execServe(c *cli.Context) error {
conf.KeepaliveInterval = keepaliveInterval conf.KeepaliveInterval = keepaliveInterval
conf.ManagerInterval = managerInterval conf.ManagerInterval = managerInterval
conf.WebRootIsApp = webRootIsApp conf.WebRootIsApp = webRootIsApp
conf.MessageLimit = int(messageSizeLimit)
conf.SMTPSenderAddr = smtpSenderAddr conf.SMTPSenderAddr = smtpSenderAddr
conf.SMTPSenderUser = smtpSenderUser conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass conf.SMTPSenderPass = smtpSenderPass

View File

@@ -399,8 +399,10 @@ HTTP challenge. I've found [this guide](https://nandovieira.com/using-lets-encry
be incredibly helpful. be incredibly helpful.
### nginx/Apache2/caddy ### nginx/Apache2/caddy
For your convenience, here's a working config that'll help configure things behind a proxy. In this For your convenience, here's a working config that'll help configure things behind a proxy. Be sure to **enable WebSockets**
example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic by forwarding the `Connection` and `Upgrade` headers accordingly.
In this example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
or the root domain: or the root domain:
=== "nginx (/etc/nginx/sites-*/ntfy)" === "nginx (/etc/nginx/sites-*/ntfy)"

View File

@@ -4,16 +4,24 @@ This page is used to list deprecation notices for ntfy. Deprecated commands and
## Active deprecations ## Active deprecations
### Android app: Using `since=<timestamp>` instead of `since=<id>` ### Android app: WebSockets will become the default connection protocol
> since 2022-02-27 > since 2022-03-13, behavior will change in **June 2022**
In future versions of the Android app, instant delivery connections and connections to self-hosted servers will
be using the WebSockets protocol. This potentially requires [configuration changes in your proxy](https://ntfy.sh/docs/config/#nginxapache2caddy).
### Android app: Using `since=<timestamp>` instead of `since=<id>`
> since 2022-02-27, behavior will change in **May 2022**
In about 3 months, the Android app will start using `since=<id>` instead of `since=<timestamp>`, which means that it will In about 3 months, the Android app will start using `since=<id>` instead of `since=<timestamp>`, which means that it will
not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app. not work with servers older than v1.16.0 anymore. This is to simplify handling of deduplication in the Android app.
The `since=<timestamp>` endpoint will continue to work. This is merely a notice that the Android app behavior will change. The `since=<timestamp>` endpoint will continue to work. This is merely a notice that the Android app behavior will change.
## Previous deprecations
### Running server via `ntfy` (instead of `ntfy serve`) ### Running server via `ntfy` (instead of `ntfy serve`)
> since 2021-12-17 > deprecated 2021-12-17, behavior changed with v1.10.0
As more commands are added to the `ntfy` CLI tool, using just `ntfy` to run the server is not practical As more commands are added to the `ntfy` CLI tool, using just `ntfy` to run the server is not practical
anymore. Please use `ntfy serve` instead. This also applies to Docker images, as they can also execute more than anymore. Please use `ntfy serve` instead. This also applies to Docker images, as they can also execute more than

View File

@@ -16,6 +16,27 @@ rsync -a root@laptop /backups/laptop \
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups || curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
``` ```
## Low disk space alerts
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
effective.
``` bash
#!/bin/bash
mingigs=10
avail=$(df | awk '$6 == "/" && $4 < '$mingigs' * 1024*1024 { print $4/1024/1024 }')
topicurl=https://ntfy.sh/mytopic
if [ -n "$avail" ]; then
curl \
-d "Only $avail GB available on the root disk. Better clean that up." \
-H "Title: Low disk space alert on $(hostname)" \
-H "Priority: high" \
-H "Tags: warning,cd" \
$topicurl
fi
```
## Server-sent messages in your web app ## Server-sent messages in your web app
Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own
web application. Check out the <a href="/example.html">live example</a> or just look the source of this page. web application. Check out the <a href="/example.html">live example</a> or just look the source of this page.
@@ -93,3 +114,13 @@ Or, if you only want to send notifications using shoutrrr:
``` ```
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage" shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
``` ```
## Random cronjobs
Alright, here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
~
```

View File

@@ -33,10 +33,11 @@ If you do not care for Firebase, I suggest you install the [F-Droid version](htt
of the app and [self-host your own ntfy server](install.md). of the app and [self-host your own ntfy server](install.md).
## How much battery does the Android app use? ## How much battery does the Android app use?
If you use the ntfy.sh server and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature, If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
the Android app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server, the Android app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
or you use *instant delivery*, the app has to maintain a constant connection to the server, which consumes about 4% of or you use *instant delivery*, the app has to maintain a constant connection to the server, which consumes about 0-1% of
battery in 17h of use (on my phone). I use it, and it makes no difference to me. battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
decent now.
## What is instant delivery? ## What is instant delivery?
[Instant delivery](subscribe/phone.md#instant-delivery) is a feature in the Android app. If turned on, the app maintains a constant connection to the [Instant delivery](subscribe/phone.md#instant-delivery) is a feature in the Android app. If turned on, the app maintains a constant connection to the

View File

@@ -26,21 +26,21 @@ deb/rpm packages.
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_x86_64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_x86_64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy serve sudo ./ntfy serve
``` ```
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_armv7.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_armv7.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy serve sudo ./ntfy serve
``` ```
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_arm64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_arm64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy serve sudo ./ntfy serve
``` ```
@@ -88,7 +88,7 @@ Manually installing the .deb file:
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_amd64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -96,7 +96,7 @@ Manually installing the .deb file:
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_armv7.deb wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -104,7 +104,7 @@ Manually installing the .deb file:
=== "arm64" === "arm64"
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_arm64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
@@ -114,21 +114,21 @@ Manually installing the .deb file:
=== "x86_64/amd64" === "x86_64/amd64"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_amd64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_amd64.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
=== "armv7/armhf" === "armv7/armhf"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_armv7.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_armv7.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```
=== "arm64" === "arm64"
```bash ```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.0/ntfy_1.17.0_linux_arm64.rpm sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.17.1/ntfy_1.17.1_linux_arm64.rpm
sudo systemctl enable ntfy sudo systemctl enable ntfy
sudo systemctl start ntfy sudo systemctl start ntfy
``` ```

View File

@@ -2,6 +2,36 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases) 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). and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
## ntfy server v1.18.0
Released XXXXXXXXXXXXXXX
**Bug fixes:**
* rpm: do not overwrite server.yaml on package upgrade (#166, thanks @waclaw66 for reporting)
**Deprecations:**
* Removed the ability to run server as `ntfy serve` as per [deprecation](https://ntfy.sh/docs/deprecations)
## ntfy Android app v1.10.0
Released XXXXXXXXXXXXXXX
**Features:**
* Support for UnifiedPush 2.0 specification (bytes messages, #130)
* Export/import settings and subscriptions (#115, thanks @cmeis for reporting)
**Bug fixes:**
* Display locale-specific times, with AM/PM or 24h format (#140, thanks @hl2guide for reporting)
## ntfy server v1.17.1
Released Mar 12, 2022
**Bug fixes:**
* Replace `crypto.subtle` with `hashCode` to errors with Brave/FF-Windows (#157, thanks for reporting @arminus)
## ntfy server v1.17.0 ## ntfy server v1.17.0
Released Mar 11, 2022 Released Mar 11, 2022

View File

@@ -130,19 +130,21 @@ notification popups:
Here's a list of extras you can access. Most likely, you'll want to filter for `topic` and react on `message`: Here's a list of extras you can access. Most likely, you'll want to filter for `topic` and react on `message`:
| Extra name | Type | Example | Description | | Extra name | Type | Example | Description |
|---|---|---|---| |-----------------|------------------------------|--------------------|------------------------------------------------------------------------------------|
| `id` | *string* | `bP8dMjO8ig` | Randomly chosen message identifier (likely not very useful for task automation) | | `id` | *String* | `bP8dMjO8ig` | Randomly chosen message identifier (likely not very useful for task automation) |
| `base_url` | *string* | `https://ntfy.sh` | Root URL of the ntfy server this message came from | | `base_url` | *String* | `https://ntfy.sh` | Root URL of the ntfy server this message came from |
| `topic` ❤️ | *string* | `mytopic` | Topic name; **you'll likely want to filter for a specific topic** | | `topic` ❤️ | *String* | `mytopic` | Topic name; **you'll likely want to filter for a specific topic** |
| `muted` | *bool* | `true` | Indicates whether the subscription was muted in the app | | `muted` | *Boolean* | `true` | Indicates whether the subscription was muted in the app |
| `muted_str` | *string (`true` or `false`)* | `true` | Same as `muted`, but as string `true` or `false` | | `muted_str` | *String (`true` or `false`)* | `true` | Same as `muted`, but as string `true` or `false` |
| `time` | *int* | `1635528741` | Message date time, as Unix time stamp | | `time` | *Int* | `1635528741` | Message date time, as Unix time stamp |
| `title` | *string* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set | | `title` | *String* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set |
| `message` ❤️ | *string* | `Some message` | Message body; **this is likely what you're interested in** | | `message` ❤️ | *String* | `Some message` | Message body; **this is likely what you're interested in** |
| `tags` | *string* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) | | `message_bytes` | *ByteArray* | `(binary data)` | Message body as binary data |
| `tags_map` | *string* | `0=tag1,1=tag2,..` | Map of tags to make it easier to map first, second, ... tag | | `encoding` | *String* | - | Message encoding (empty or "base64") |
| `priority` | *int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max | | `tags` | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `tags_map` | *String* | `0=tag1,1=tag2,..` | Map of tags to make it easier to map first, second, ... tag |
| `priority` | *Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
#### Send messages using intents #### Send messages using intents
To send messages from other apps (such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) To send messages from other apps (such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid)
@@ -164,14 +166,14 @@ Here's what that looks like:
The following intent extras are supported when for the intent with the `io.heckel.ntfy.SEND_MESSAGE` action: The following intent extras are supported when for the intent with the `io.heckel.ntfy.SEND_MESSAGE` action:
| Extra name | Required | Type | Example | Description | | Extra name | Required | Type | Example | Description |
|---|---|---|---|---| |--------------|----------|-------------------------------|-------------------|------------------------------------------------------------------------------------|
| `base_url` | - | *string* | `https://ntfy.sh` | Root URL of the ntfy server this message came from, defaults to `https://ntfy.sh` | | `base_url` | - | *String* | `https://ntfy.sh` | Root URL of the ntfy server this message came from, defaults to `https://ntfy.sh` |
| `topic` ❤️ | ✔ | *string* | `mytopic` | Topic name; **you must set this** | | `topic` ❤️ | ✔ | *String* | `mytopic` | Topic name; **you must set this** |
| `title` | - | *string* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set | | `title` | - | *String* | `Some title` | Message [title](../publish.md#message-title); may be empty if not set |
| `message` ❤️ | ✔ | *string* | `Some message` | Message body; **you must set this** | | `message` ❤️ | ✔ | *String* | `Some message` | Message body; **you must set this** |
| `tags` | - | *string* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) | | `tags` | - | *String* | `tag1,tag2,..` | Comma-separated list of [tags](../publish.md#tags-emojis) |
| `priority` | - | *string or int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max | | `priority` | - | *String or Int (between 1-5)* | `4` | Message [priority](../publish.md#message-priority) with 1=min, 3=default and 5=max |
## iPhone/iOS ## iPhone/iOS
I almost feel devious for putting the *Download on the App Store* button on this page. Currently, there is no iOS app I almost feel devious for putting the *Download on the App Store* button on this page. Currently, there is no iOS app

3
go.mod
View File

@@ -14,6 +14,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.11 github.com/mattn/go-sqlite3 v1.14.11
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.14.0
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
@@ -38,6 +39,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
go.opencensus.io v0.23.0 // indirect go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect

6
go.sum
View File

@@ -223,6 +223,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@@ -21,7 +21,8 @@ const (
// - total topic limit: max number of topics overall // - total topic limit: max number of topics overall
// - various attachment limits // - various attachment limits
const ( const (
DefaultMessageLengthLimit = 4096 // Bytes DefaultMessageLengthLimit = 4096 // Bytes
MaxMessageLengthLimit = 16 * 1024 * 1024 // 16 MB, sanity size
DefaultTotalTopicLimit = 15000 DefaultTotalTopicLimit = 15000
DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB

View File

@@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/tidwall/gjson"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"heckel.io/ntfy/auth" "heckel.io/ntfy/auth"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
@@ -397,11 +398,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
return err return err
} }
m := newDefaultMessage(t.ID, "") m := newDefaultMessage(t.ID, "")
cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m) cache, firebase, email, template, unifiedpush, err := s.parsePublishParams(r, v, m)
if err != nil { if err != nil {
return err return err
} }
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil { if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
return err return err
} }
if m.Message == "" { if m.Message == "" {
@@ -443,7 +444,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
return nil return nil
} }
func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) { func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, template string, unifiedpush bool, err error) {
cache = readBoolParam(r, true, "x-cache", "cache") cache = readBoolParam(r, true, "x-cache", "cache")
firebase = readBoolParam(r, true, "x-firebase", "firebase") firebase = readBoolParam(r, true, "x-firebase", "firebase")
m.Title = readParam(r, "x-title", "title", "t") m.Title = readParam(r, "x-title", "title", "t")
@@ -458,7 +459,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
} }
if attach != "" { if attach != "" {
if !attachURLRegex.MatchString(attach) { if !attachURLRegex.MatchString(attach) {
return false, false, "", false, errHTTPBadRequestAttachmentURLInvalid return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
} }
m.Attachment.URL = attach m.Attachment.URL = attach
if m.Attachment.Name == "" { if m.Attachment.Name == "" {
@@ -477,11 +478,11 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if email != "" { if email != "" {
if err := v.EmailAllowed(); err != nil { if err := v.EmailAllowed(); err != nil {
return false, false, "", false, errHTTPTooManyRequestsLimitEmails return false, false, "", "", false, errHTTPTooManyRequestsLimitEmails
} }
} }
if s.mailer == nil && email != "" { if s.mailer == nil && email != "" {
return false, false, "", false, errHTTPBadRequestEmailDisabled return false, false, "", "", false, errHTTPBadRequestEmailDisabled
} }
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
if messageStr != "" { if messageStr != "" {
@@ -489,7 +490,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
} }
m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
if err != nil { if err != nil {
return false, false, "", false, errHTTPBadRequestPriorityInvalid return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
} }
tagsStr := readParam(r, "x-tags", "tags", "tag", "ta") tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
if tagsStr != "" { if tagsStr != "" {
@@ -501,27 +502,33 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
if delayStr != "" { if delayStr != "" {
if !cache { if !cache {
return false, false, "", false, errHTTPBadRequestDelayNoCache return false, false, "", "", false, errHTTPBadRequestDelayNoCache
} }
if email != "" { if email != "" {
return false, false, "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
} }
delay, err := util.ParseFutureTime(delayStr, time.Now()) delay, err := util.ParseFutureTime(delayStr, time.Now())
if err != nil { if err != nil {
return false, false, "", false, errHTTPBadRequestDelayCannotParse return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() { } else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
return false, false, "", false, errHTTPBadRequestDelayTooSmall return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() { } else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
return false, false, "", false, errHTTPBadRequestDelayTooLarge return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
} }
m.Time = delay.Unix() m.Time = delay.Unix()
} }
template = readParam(r, "x-template", "template", "tpl")
if template != "" {
if template != "json" {
return false, false, "", "", false, errors.New("invalid template")
}
}
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush { if unifiedpush {
firebase = false firebase = false
unifiedpush = true unifiedpush = true
} }
return cache, firebase, email, unifiedpush, nil return cache, firebase, email, template, unifiedpush, nil
} }
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
@@ -536,15 +543,15 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
// 5. curl -T file.txt ntfy.sh/mytopic // 5. curl -T file.txt ntfy.sh/mytopic
// If file.txt is > message limit, treat it as an attachment // If file.txt is > message limit, treat it as an attachment
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser, unifiedpush bool) error { func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser, template string, unifiedpush bool) error {
if unifiedpush { if unifiedpush {
return s.handleBodyAsMessageAutoDetect(m, body) // Case 1 return s.handleBodyAsMessageAutoDetect(m, body) // Case 1
} else if m.Attachment != nil && m.Attachment.URL != "" { } else if m.Attachment != nil && m.Attachment.URL != "" {
return s.handleBodyAsTextMessage(m, body) // Case 2 return s.handleBodyAsTextMessage(m, body, template) // Case 2
} else if m.Attachment != nil && m.Attachment.Name != "" { } else if m.Attachment != nil && m.Attachment.Name != "" {
return s.handleBodyAsAttachment(r, v, m, body) // Case 3 return s.handleBodyAsAttachment(r, v, m, body) // Case 3
} else if !body.LimitReached && utf8.Valid(body.PeakedBytes) { } else if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
return s.handleBodyAsTextMessage(m, body) // Case 4 return s.handleBodyAsTextMessage(m, body, template) // Case 4
} }
return s.handleBodyAsAttachment(r, v, m, body) // Case 5 return s.handleBodyAsAttachment(r, v, m, body) // Case 5
} }
@@ -559,12 +566,33 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeakedRead
return nil return nil
} }
func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeakedReadCloser) error { func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeakedReadCloser, template string) error {
if !utf8.Valid(body.PeakedBytes) { if !utf8.Valid(body.PeakedBytes) {
return errHTTPBadRequestMessageNotUTF8 return errHTTPBadRequestMessageNotUTF8
} }
if len(body.PeakedBytes) > 0 { // Empty body should not override message (publish via GET!) if len(body.PeakedBytes) > 0 { // Empty body should not override message (publish via GET!)
m.Message = strings.TrimSpace(string(body.PeakedBytes)) // Truncates the message to the peak limit if required peakedBody := strings.TrimSpace(string(body.PeakedBytes)) // Truncates the message to the peak limit if required
if template == "json" && gjson.Valid(peakedBody) {
r := regexp.MustCompile(`\${([^}]+)}`)
matches := r.FindAllStringSubmatch(m.Message, -1)
for _, v := range matches {
query := v[1]
result := gjson.Get(peakedBody, query)
if result.Exists() {
m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String())
}
}
matches = r.FindAllStringSubmatch(m.Title, -1)
for _, v := range matches {
query := v[1]
result := gjson.Get(peakedBody, query)
if result.Exists() {
m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String())
}
}
} else {
m.Message = peakedBody
}
} }
if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" {
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)

View File

@@ -870,7 +870,7 @@ func TestServer_PublishAttachment(t *testing.T) {
require.Equal(t, "attachment.txt", msg.Attachment.Name) require.Equal(t, "attachment.txt", msg.Attachment.Name)
require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type)
require.Equal(t, int64(5000), msg.Attachment.Size) require.Equal(t, int64(5000), msg.Attachment.Size)
require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix()) require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
require.Equal(t, "", msg.Attachment.Owner) // Should never be returned require.Equal(t, "", msg.Attachment.Owner) // Should never be returned
require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID))

View File

@@ -1,5 +1,5 @@
import Connection from "./Connection"; import Connection from "./Connection";
import {sha256} from "./utils"; import {hashCode} from "./utils";
/** /**
* The connection manager keeps track of active connections (WebSocket connections, see Connection). * The connection manager keeps track of active connections (WebSocket connections, see Connection).
@@ -108,10 +108,9 @@ class ConnectionManager {
} }
const makeConnectionId = async (subscription, user) => { const makeConnectionId = async (subscription, user) => {
const hash = (user) return (user)
? await sha256(`${subscription.id}|${user.username}|${user.password}`) ? hashCode(`${subscription.id}|${user.username}|${user.password}`)
: await sha256(`${subscription.id}`); : hashCode(`${subscription.id}`);
return hash.substring(0, 10);
} }
const connectionManager = new ConnectionManager(); const connectionManager = new ConnectionManager();

View File

@@ -115,10 +115,15 @@ export const shuffle = (arr) => {
return arr; return arr;
} }
// https://jameshfisher.com/2017/10/30/web-cryptography-api-hello-world/ /** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */
export const sha256 = async (s) => { export const hashCode = async (s) => {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder("utf-8").encode(s)); let hash = 0;
return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join(''); for (let i = 0; i < s.length; i++) {
const char = s.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
} }
export const formatShortDateTime = (timestamp) => { export const formatShortDateTime = (timestamp) => {