mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-22 03:13:33 +01:00
Emojis in notifications; server caching
This commit is contained in:
parent
052ab7d411
commit
8616be12a2
6 changed files with 159 additions and 64 deletions
|
@ -213,6 +213,6 @@ Third party libraries and resources:
|
|||
* [GoReleaser](https://goreleaser.com/) (MIT) is used to create releases
|
||||
* [github.com/mattn/go-sqlite3](https://github.com/mattn/go-sqlite3) (MIT) is used to provide the persistent message cache
|
||||
* [Firebase Admin SDK](https://github.com/firebase/firebase-admin-go) (Apache 2.0) is used to send FCM messages
|
||||
* [emoji-java](https://github.com/vdurmont/emoji-java) (MIT) is used for emoji support (the emoji.json file only)
|
||||
* [github/gemoji](https://github.com/github/gemoji) (MIT) is used for emoji support (specifically the [emoji.json](https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json) file)
|
||||
* [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox)
|
||||
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
curl \<br/>
|
||||
-H "Title: Unauthorized access detected" \<br/>
|
||||
-H "Priority: urgent" \<br/>
|
||||
-H "Tags: warn,skull" \<br/>
|
||||
-H "Tags: warning,skull" \<br/>
|
||||
-d "Remote access to $(hostname) detected. Act right away." \<br/>
|
||||
<span class="ntfyUrl">ntfy.sh</span>/mytopic
|
||||
</code>
|
||||
|
@ -228,16 +228,22 @@
|
|||
curl -H "Title: Dogs are better than cats" -d "Oh my ..." <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
|
||||
</code>
|
||||
|
||||
<h3 id="tags" class="anchor">Tagging messages (<tt>X-Tags</tt>, <tt>Tags</tt>, or <tt>ta</tt>)</h3>
|
||||
<h3 id="tags" class="anchor">Tags & emojis 🥳 🎉 (<tt>X-Tags</tt>, <tt>Tags</tt>, or <tt>ta</tt>)</h3>
|
||||
<p>
|
||||
You can tag messages with emojis (or other relevant strings). If a tag matches a <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">known emoji short code</a>,
|
||||
it will be converted to an emoji. If it doesn't match, it will be listed below the notification. This is useful
|
||||
for things like warnings and such (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. which script the
|
||||
message came from, ...).
|
||||
</p>
|
||||
<p class="smallMarginBottom">
|
||||
You can tag notifications with emojis (or other relevant strings). In the phone app, the tags will be converted
|
||||
to emojis and prepended to the message or title in the notification. You can set tags with the <tt>X-Tags</tt> header
|
||||
(or any of its aliases: <tt>Tags</tt>, or <tt>ta</tt>). Use <a href="https://github.com/vdurmont/emoji-java/blob/master/EMOJIS.md">this reference</a>
|
||||
to figure out what tags you can use to send emojis.
|
||||
You can set tags with the <tt>X-Tags</tt> header (or any of its aliases: <tt>Tags</tt>, or <tt>ta</tt>).
|
||||
Use <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">this reference</a>
|
||||
to figure out what tags can be converted to emojis. In the example below, the tag "warning" matches the emoji ⚠️,
|
||||
the tag "ssh-login" doesn't match and will be displayed below the message.
|
||||
</p>
|
||||
<code>
|
||||
curl -H "Tags: warn,skull" -d "Unauthorized SSH access" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
|
||||
curl -H tags:thumbsup -d "Backup successful" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
|
||||
$ curl -H "Tags: warning,ssh-login" -d "Unauthorized SSH access" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
|
||||
{"id":"ZEIwjfHlSS",...,"tags":["warning","ssh-login"],"message":"Unauthorized SSH access"}
|
||||
</code>
|
||||
|
||||
<h2 id="examples" class="anchor">Examples</h2>
|
||||
|
@ -257,7 +263,7 @@
|
|||
rsync -a root@laptop /backups/laptop \<br/>
|
||||
&& zfs snapshot ... \<br/>
|
||||
&& curl -d "Laptop backup succeeded" <span class="ntfyUrl">ntfy.sh</span>/backups \<br/>
|
||||
|| curl -H tags:warn -H prio:high -d "Laptop backup failed" <span class="ntfyUrl">ntfy.sh</span>/backups
|
||||
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" <span class="ntfyUrl">ntfy.sh</span>/backups
|
||||
</code>
|
||||
|
||||
<h3 id="example-web" class="anchor">Example: Server-sent messages in your web app</h3>
|
||||
|
@ -284,7 +290,7 @@
|
|||
<code>
|
||||
#!/bin/bash<br/>
|
||||
if [ "${PAM_TYPE}" = "open_session" ]; then<br/>
|
||||
curl -H tags:warn -d "SSH login: ${PAM_USER} from ${PAM_RHOST}" <span class="ntfyUrl">ntfy.sh</span>/alerts<br/>
|
||||
curl -H tags:warning -d "SSH login: ${PAM_USER} from ${PAM_RHOST}" <span class="ntfyUrl">ntfy.sh</span>/alerts<br/>
|
||||
fi
|
||||
</code>
|
||||
|
||||
|
@ -405,6 +411,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="lightbox" class="lightbox"></div>
|
||||
<script src="static/js/emoji.js"></script>
|
||||
<script src="static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -93,6 +93,7 @@ var (
|
|||
|
||||
//go:embed static
|
||||
webStaticFs embed.FS
|
||||
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
|
||||
|
||||
errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)}
|
||||
errHTTPNotFound = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)}
|
||||
|
@ -231,7 +232,7 @@ func (s *Server) handleExample(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
|
||||
http.FileServer(http.FS(webStaticFs)).ServeHTTP(w, r)
|
||||
http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -86,9 +86,10 @@ const subscribeInternal = (topic, persist, delaySec) => {
|
|||
}
|
||||
if (Notification.permission === "granted") {
|
||||
notifySound.play();
|
||||
const title = (event.title) ? event.title : `${location.host}/${topic}`;
|
||||
const title = formatTitle(event);
|
||||
const message = formatMessage(event);
|
||||
const notification = new Notification(title, {
|
||||
body: event.message,
|
||||
body: message,
|
||||
icon: '/static/img/favicon.png'
|
||||
});
|
||||
notification.onclick = (e) => {
|
||||
|
@ -158,56 +159,28 @@ const rerenderDetailView = () => {
|
|||
const messageDiv = document.createElement('div');
|
||||
const tagsDiv = document.createElement('div');
|
||||
|
||||
// Figure out mapped emojis (and unmapped tags)
|
||||
let mappedEmojiTags = '';
|
||||
let unmappedTags = '';
|
||||
if (m.tags) {
|
||||
mappedEmojiTags = m.tags
|
||||
.filter(tag => tag in emojis)
|
||||
.map(tag => emojis[tag])
|
||||
.join("");
|
||||
unmappedTags = m.tags
|
||||
.filter(tag => !(tag in emojis))
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
// Figure out title and message
|
||||
let title = '';
|
||||
let message = m.message;
|
||||
if (m.title) {
|
||||
if (mappedEmojiTags) {
|
||||
title = `${mappedEmojiTags} ${m.title}`;
|
||||
} else {
|
||||
title = m.title;
|
||||
}
|
||||
} else {
|
||||
if (mappedEmojiTags) {
|
||||
message = `${mappedEmojiTags} ${m.message}`;
|
||||
} else {
|
||||
message = m.message;
|
||||
}
|
||||
}
|
||||
|
||||
entryDiv.classList.add('detailEntry');
|
||||
dateDiv.classList.add('detailDate');
|
||||
titleDiv.classList.add('detailTitle');
|
||||
messageDiv.classList.add('detailMessage');
|
||||
tagsDiv.classList.add('detailTags');
|
||||
|
||||
const dateStr = new Date(m.time * 1000).toLocaleString();
|
||||
if (m.priority && [1,2,4,5].includes(m.priority)) {
|
||||
dateDiv.innerHTML = `${dateStr} <img src="static/img/priority-${m.priority}.svg"/>`;
|
||||
} else {
|
||||
dateDiv.innerHTML = `${dateStr}`;
|
||||
}
|
||||
messageDiv.classList.add('detailMessage');
|
||||
messageDiv.innerText = message;
|
||||
messageDiv.innerText = formatMessage(m);
|
||||
entryDiv.appendChild(dateDiv);
|
||||
if (m.title) {
|
||||
titleDiv.classList.add('detailTitle');
|
||||
titleDiv.innerText = title;
|
||||
titleDiv.innerText = formatTitleA(m);
|
||||
entryDiv.appendChild(titleDiv);
|
||||
}
|
||||
entryDiv.appendChild(messageDiv);
|
||||
if (unmappedTags) {
|
||||
tagsDiv.classList.add('detailTags');
|
||||
tagsDiv.innerText = `Tags: ${unmappedTags}`;
|
||||
const otherTags = unmatchedTags(m.tags);
|
||||
if (otherTags.length > 0) {
|
||||
tagsDiv.innerText = `Tags: ${otherTags.join(", ")}`;
|
||||
entryDiv.appendChild(tagsDiv);
|
||||
}
|
||||
detailEventsList.appendChild(entryDiv);
|
||||
|
@ -311,10 +284,46 @@ const nextScreenshotKeyboardListener = (e) => {
|
|||
}
|
||||
};
|
||||
|
||||
const toEmoji = (tag) => {
|
||||
emojis
|
||||
const formatTitle = (m) => {
|
||||
if (m.title) {
|
||||
return formatTitleA(m);
|
||||
} else {
|
||||
return `${location.host}/${m.topic}`;
|
||||
}
|
||||
};
|
||||
|
||||
const formatTitleA = (m) => {
|
||||
const emojiList = toEmojis(m.tags);
|
||||
if (emojiList) {
|
||||
return `${emojiList.join(" ")} ${m.title}`;
|
||||
} else {
|
||||
return m.title;
|
||||
}
|
||||
};
|
||||
|
||||
const formatMessage = (m) => {
|
||||
if (m.title) {
|
||||
return m.message;
|
||||
} else {
|
||||
const emojiList = toEmojis(m.tags);
|
||||
if (emojiList) {
|
||||
return `${emojiList.join(" ")} ${m.message}`;
|
||||
} else {
|
||||
return m.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toEmojis = (tags) => {
|
||||
if (!tags) return [];
|
||||
else return tags.filter(tag => tag in emojis).map(tag => emojis[tag]);
|
||||
}
|
||||
|
||||
const unmatchedTags = (tags) => {
|
||||
if (!tags) return [];
|
||||
else return tags.filter(tag => !(tag in emojis));
|
||||
}
|
||||
|
||||
// From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
|
||||
async function* makeTextFileLineIterator(fileURL) {
|
||||
const utf8Decoder = new TextDecoder('utf-8');
|
||||
|
@ -417,14 +426,10 @@ document.querySelectorAll('.ntfyProtocol').forEach((el) => {
|
|||
el.innerHTML = window.location.protocol + "//";
|
||||
});
|
||||
|
||||
// Fetch emojis
|
||||
// Format emojis (see emoji.js)
|
||||
const emojis = {};
|
||||
fetch('static/js/emoji.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.forEach(emoji => {
|
||||
rawEmojis.forEach(emoji => {
|
||||
emoji.aliases.forEach(alias => {
|
||||
emojis[alias] = emoji.emoji;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
[
|
||||
// Original data source: https://github.com/github/gemoji/blob/master/db/emoji.json
|
||||
// Manually prepended "const rawEmojis = " to make it play nice with JS/HTML.
|
||||
|
||||
const rawEmojis = [
|
||||
{
|
||||
"emoji": "😀"
|
||||
, "description": "grinning face"
|
79
util/embedfs.go
Normal file
79
util/embedfs.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CachingEmbedFS struct {
|
||||
ModTime time.Time
|
||||
FS embed.FS
|
||||
}
|
||||
|
||||
func (e CachingEmbedFS) Open(name string) (fs.File, error) {
|
||||
f, err := e.FS.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cachingEmbedFile{f, e.ModTime}, nil
|
||||
}
|
||||
|
||||
type cachingEmbedFile struct {
|
||||
file fs.File
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (f cachingEmbedFile) Stat() (fs.FileInfo, error) {
|
||||
s, err := f.file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &etagEmbedFileInfo{s, f.modTime}, nil
|
||||
}
|
||||
|
||||
func (f cachingEmbedFile) Read(bytes []byte) (int, error) {
|
||||
return f.file.Read(bytes)
|
||||
}
|
||||
|
||||
func (f *cachingEmbedFile) Seek(offset int64, whence int) (int64, error) {
|
||||
if seeker, ok := f.file.(io.Seeker); ok {
|
||||
return seeker.Seek(offset, whence)
|
||||
}
|
||||
return 0, errors.New("io.Seeker not implemented")
|
||||
}
|
||||
|
||||
func (f cachingEmbedFile) Close() error {
|
||||
return f.file.Close()
|
||||
}
|
||||
|
||||
type etagEmbedFileInfo struct {
|
||||
file fs.FileInfo
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) Name() string {
|
||||
return e.file.Name()
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) Size() int64 {
|
||||
return e.file.Size()
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) Mode() fs.FileMode {
|
||||
return e.file.Mode()
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) ModTime() time.Time {
|
||||
return e.modTime // We override this!
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) IsDir() bool {
|
||||
return e.file.IsDir()
|
||||
}
|
||||
|
||||
func (e etagEmbedFileInfo) Sys() interface{} {
|
||||
return e.file.Sys()
|
||||
}
|
Loading…
Reference in a new issue