Emojis in notifications; server caching

pull/29/head
Philipp Heckel 2021-11-29 09:34:43 -05:00
parent 052ab7d411
commit 8616be12a2
6 changed files with 159 additions and 64 deletions

View File

@ -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)

View File

@ -85,7 +85,7 @@
curl \<br/>
&nbsp;&nbsp;-H "Title: Unauthorized access detected" \<br/>
&nbsp;&nbsp;-H "Priority: urgent" \<br/>
&nbsp;&nbsp;-H "Tags: warn,skull" \<br/>
&nbsp;&nbsp;-H "Tags: warning,skull" \<br/>
&nbsp;&nbsp;-d "Remote access to $(hostname) detected. Act right away." \<br/>
&nbsp;&nbsp;<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 &amp; 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/>
&nbsp;&nbsp;&& zfs snapshot ... \<br/>
&nbsp;&nbsp;&& curl -d "Laptop backup succeeded" <span class="ntfyUrl">ntfy.sh</span>/backups \<br/>
&nbsp;&nbsp;|| curl -H tags:warn -H prio:high -d "Laptop backup failed" <span class="ntfyUrl">ntfy.sh</span>/backups
&nbsp;&nbsp;|| 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/>
&nbsp;&nbsp;curl -H tags:warn -d "SSH login: ${PAM_USER} from ${PAM_RHOST}" <span class="ntfyUrl">ntfy.sh</span>/alerts<br/>
&nbsp;&nbsp;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>

View File

@ -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
}

View File

@ -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;
});
});
});
});

View File

@ -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 100644
View 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()
}