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 * [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 * [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 * [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) * [Lightbox with vanilla JS](https://yossiabramov.com/blog/vanilla-js-lightbox)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html) * [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)

View File

@ -85,7 +85,7 @@
curl \<br/> curl \<br/>
&nbsp;&nbsp;-H "Title: Unauthorized access detected" \<br/> &nbsp;&nbsp;-H "Title: Unauthorized access detected" \<br/>
&nbsp;&nbsp;-H "Priority: urgent" \<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;-d "Remote access to $(hostname) detected. Act right away." \<br/>
&nbsp;&nbsp;<span class="ntfyUrl">ntfy.sh</span>/mytopic &nbsp;&nbsp;<span class="ntfyUrl">ntfy.sh</span>/mytopic
</code> </code>
@ -228,16 +228,22 @@
curl -H "Title: Dogs are better than cats" -d "Oh my ..." <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/> curl -H "Title: Dogs are better than cats" -d "Oh my ..." <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/>
</code> </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"> <p class="smallMarginBottom">
You can tag notifications with emojis (or other relevant strings). In the phone app, the tags will be converted You can set tags with the <tt>X-Tags</tt> header (or any of its aliases: <tt>Tags</tt>, or <tt>ta</tt>).
to emojis and prepended to the message or title in the notification. You can set tags with the <tt>X-Tags</tt> header Use <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">this reference</a>
(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 can be converted to emojis. In the example below, the tag "warning" matches the emoji ⚠️,
to figure out what tags you can use to send emojis. the tag "ssh-login" doesn't match and will be displayed below the message.
</p> </p>
<code> <code>
curl -H "Tags: warn,skull" -d "Unauthorized SSH access" <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/>
curl -H tags:thumbsup -d "Backup successful" <span class="ntfyUrl">ntfy.sh</span>/mytopic<br/> {"id":"ZEIwjfHlSS",...,"tags":["warning","ssh-login"],"message":"Unauthorized SSH access"}
</code> </code>
<h2 id="examples" class="anchor">Examples</h2> <h2 id="examples" class="anchor">Examples</h2>
@ -257,7 +263,7 @@
rsync -a root@laptop /backups/laptop \<br/> rsync -a root@laptop /backups/laptop \<br/>
&nbsp;&nbsp;&& zfs snapshot ... \<br/> &nbsp;&nbsp;&& zfs snapshot ... \<br/>
&nbsp;&nbsp;&& curl -d "Laptop backup succeeded" <span class="ntfyUrl">ntfy.sh</span>/backups \<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> </code>
<h3 id="example-web" class="anchor">Example: Server-sent messages in your web app</h3> <h3 id="example-web" class="anchor">Example: Server-sent messages in your web app</h3>
@ -284,7 +290,7 @@
<code> <code>
#!/bin/bash<br/> #!/bin/bash<br/>
if [ "${PAM_TYPE}" = "open_session" ]; then<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 fi
</code> </code>
@ -405,6 +411,7 @@
</div> </div>
</div> </div>
<div id="lightbox" class="lightbox"></div> <div id="lightbox" class="lightbox"></div>
<script src="static/js/emoji.js"></script>
<script src="static/js/app.js"></script> <script src="static/js/app.js"></script>
</body> </body>
</html> </html>

View File

@ -93,6 +93,7 @@ var (
//go:embed static //go:embed static
webStaticFs embed.FS webStaticFs embed.FS
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)} errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)}
errHTTPNotFound = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)} 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 { 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 return nil
} }

View File

@ -86,9 +86,10 @@ const subscribeInternal = (topic, persist, delaySec) => {
} }
if (Notification.permission === "granted") { if (Notification.permission === "granted") {
notifySound.play(); notifySound.play();
const title = (event.title) ? event.title : `${location.host}/${topic}`; const title = formatTitle(event);
const message = formatMessage(event);
const notification = new Notification(title, { const notification = new Notification(title, {
body: event.message, body: message,
icon: '/static/img/favicon.png' icon: '/static/img/favicon.png'
}); });
notification.onclick = (e) => { notification.onclick = (e) => {
@ -158,56 +159,28 @@ const rerenderDetailView = () => {
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
const tagsDiv = 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'); entryDiv.classList.add('detailEntry');
dateDiv.classList.add('detailDate'); dateDiv.classList.add('detailDate');
titleDiv.classList.add('detailTitle');
messageDiv.classList.add('detailMessage');
tagsDiv.classList.add('detailTags');
const dateStr = new Date(m.time * 1000).toLocaleString(); const dateStr = new Date(m.time * 1000).toLocaleString();
if (m.priority && [1,2,4,5].includes(m.priority)) { if (m.priority && [1,2,4,5].includes(m.priority)) {
dateDiv.innerHTML = `${dateStr} <img src="static/img/priority-${m.priority}.svg"/>`; dateDiv.innerHTML = `${dateStr} <img src="static/img/priority-${m.priority}.svg"/>`;
} else { } else {
dateDiv.innerHTML = `${dateStr}`; dateDiv.innerHTML = `${dateStr}`;
} }
messageDiv.classList.add('detailMessage'); messageDiv.innerText = formatMessage(m);
messageDiv.innerText = message;
entryDiv.appendChild(dateDiv); entryDiv.appendChild(dateDiv);
if (m.title) { if (m.title) {
titleDiv.classList.add('detailTitle'); titleDiv.innerText = formatTitleA(m);
titleDiv.innerText = title;
entryDiv.appendChild(titleDiv); entryDiv.appendChild(titleDiv);
} }
entryDiv.appendChild(messageDiv); entryDiv.appendChild(messageDiv);
if (unmappedTags) { const otherTags = unmatchedTags(m.tags);
tagsDiv.classList.add('detailTags'); if (otherTags.length > 0) {
tagsDiv.innerText = `Tags: ${unmappedTags}`; tagsDiv.innerText = `Tags: ${otherTags.join(", ")}`;
entryDiv.appendChild(tagsDiv); entryDiv.appendChild(tagsDiv);
} }
detailEventsList.appendChild(entryDiv); detailEventsList.appendChild(entryDiv);
@ -311,10 +284,46 @@ const nextScreenshotKeyboardListener = (e) => {
} }
}; };
const toEmoji = (tag) => { const formatTitle = (m) => {
emojis 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 // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
async function* makeTextFileLineIterator(fileURL) { async function* makeTextFileLineIterator(fileURL) {
const utf8Decoder = new TextDecoder('utf-8'); const utf8Decoder = new TextDecoder('utf-8');
@ -417,14 +426,10 @@ document.querySelectorAll('.ntfyProtocol').forEach((el) => {
el.innerHTML = window.location.protocol + "//"; el.innerHTML = window.location.protocol + "//";
}); });
// Fetch emojis // Format emojis (see emoji.js)
const emojis = {}; const emojis = {};
fetch('static/js/emoji.json') rawEmojis.forEach(emoji => {
.then(response => response.json())
.then(data => {
data.forEach(emoji => {
emoji.aliases.forEach(alias => { emoji.aliases.forEach(alias => {
emojis[alias] = emoji.emoji; 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": "😀" "emoji": "😀"
, "description": "grinning face" , "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()
}