Philipp Heckel 3 years ago
parent
commit
317621c696

BIN
assets/favicon.xcf


+ 1 - 0
examples/example_eventsource_sse.html

@@ -1,6 +1,7 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
+    <meta charset="UTF-8">
     <title>ntfy.sh: EventSource Example</title>
     <style>
         body { font-size: 1.2em; line-height: 130%; }

+ 35 - 135
server/index.html

@@ -1,19 +1,39 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-    <title>ntfy.sh</title>
-    <style>
-        body { font-size: 1.2em; line-height: 130%; }
-        #error { color: darkred; font-style: italic; }
-        #main { max-width: 900px; margin: 0 auto 50px auto; }
-    </style>
+    <meta charset="UTF-8">
+
+    <title>ntfy.sh | simple HTTP-based pub-sub</title>
+    <link rel="stylesheet" href="static/css/app.css" type="text/css">
+
+    <!-- Mobile view -->
+    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="HandheldFriendly" content="true">
+
+    <!-- Mobile browsers, background color -->
+    <meta name="theme-color" content="#004c79">
+    <meta name="msapplication-navbutton-color" content="#004c79">
+    <meta name="apple-mobile-web-app-status-bar-style" content="#004c79">
+
+    <!-- Favicon, see favicon.io -->
+    <link rel="icon" type="image/png" href="static/img/favicon.png">
+
+    <!-- Previews in Google, Slack, WhatsApp, etc. -->
+    <meta property="og:type" content="website" />
+    <meta property="og:locale" content="en_US" />
+    <meta property="og:site_name" content="ntfy.sh" />
+    <meta property="og:title" content="ntfy.sh | simple HTTP-based pub-sub" />
+    <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
+    <meta property="og:image" content="/static/img/ntfy.png" />
+    <meta property="og:url" content="https://ntfy.sh" />
 </head>
 <body>
 <div id="main">
     <h1>ntfy.sh - simple HTTP-based pub-sub</h1>
     <p>
-        <b>ntfy</b> (pronounce: <i>notify</i>) is a simple <b>HTTP-based pub-sub notification service and tool</b>.
-        It allows you to send <b>desktop notifications via scripts</b>, entirely <b>without signup or cost</b>.
+        <b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based pub-sub notification service and tool.
+        It allows you to send <b>desktop notifications via scripts from any computer</b>, entirely <b>without signup or cost</b>.
         It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own.
     </p>
     <p id="error"></p>
@@ -37,151 +57,31 @@
         <p>
             <label for="topicField">Topic ID:</label>
             <input type="text" id="topicField" placeholder="Letters, numbers, _ and -"  pattern="[-_A-Za-z]{1,64}" autofocus />
-            <input type="submit" id="subscribeButton" value="Subscribe topic" />
+            <input type="submit" id="subscribeButton" value="Subscribe" />
         </p>
     </form>
     <p id="topicsHeader">Subscribed topics:</p>
     <ul id="topicsList"></ul>
 
     <h3>Subscribe via your app, or via the CLI</h3>
-    <tt>
+    <code>
         curl -s ntfy.sh/mytopic/raw # one message per line (\n are replaced with a space)<br/>
         curl -s ntfy.sh/mytopic/json # one JSON message per line<br/>
         curl -s ntfy.sh/mytopic/sse # server-sent events (SSE) stream
-    </tt>
+    </code>
 
-    <h3>Publishing messages</h3>
+    <h2>Publishing messages</h2>
     <p>
         Publishing messages can be done via PUT or POST using. Here's an example using <tt>curl</tt>:
     </p>
-    <tt>
+    <code>
         curl -d "long process is done" ntfy.sh/mytopic
-    </tt>
+    </code>
     <p>
         Messages published to a non-existing topic or a topic without subscribers will not be delivered later.
         There is (currently) no buffering of any kind. If you're not listening, the message won't be delivered.
     </p>
 </div>
-
-<script type="text/javascript">
-    let topics = {};
-
-    const topicsHeader = document.getElementById("topicsHeader");
-    const topicsList = document.getElementById("topicsList");
-    const topicField = document.getElementById("topicField");
-    const subscribeButton = document.getElementById("subscribeButton");
-    const subscribeForm = document.getElementById("subscribeForm");
-    const errorField = document.getElementById("error");
-
-    const subscribe = (topic) => {
-        if (Notification.permission !== "granted") {
-            Notification.requestPermission().then((permission) => {
-                if (permission === "granted") {
-                    subscribeInternal(topic, 0);
-                } else {
-                    showNotificationDeniedError();
-                }
-            });
-        } else {
-            subscribeInternal(topic, 0);
-        }
-    };
-
-    const subscribeInternal = (topic, delaySec) => {
-        setTimeout(() => {
-            // Render list entry
-            let topicEntry = document.getElementById(`topic-${topic}`);
-            if (!topicEntry) {
-                topicEntry = document.createElement('li');
-                topicEntry.id = `topic-${topic}`;
-                topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
-                topicsList.appendChild(topicEntry);
-            }
-            topicsHeader.style.display = '';
-
-            // Open event source
-            let eventSource = new EventSource(`${topic}/sse`);
-            eventSource.onopen = () => {
-                topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
-                delaySec = 0; // Reset on successful connection
-            };
-            eventSource.onerror = (e) => {
-                const newDelaySec = (delaySec + 5 <= 15) ? delaySec + 5 : 15;
-                topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button disabled="disabled">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
-                eventSource.close()
-                subscribeInternal(topic, newDelaySec);
-            };
-            eventSource.onmessage = (e) => {
-                const event = JSON.parse(e.data);
-                new Notification(event.message);
-            };
-            topics[topic] = eventSource;
-            localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
-        }, delaySec * 1000);
-    };
-
-    const unsubscribe = (topic) => {
-        topics[topic].close();
-        delete topics[topic];
-        localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
-        document.getElementById(`topic-${topic}`).remove();
-        if (Object.keys(topics).length === 0) {
-            topicsHeader.style.display = 'none';
-        }
-    };
-
-    const test = (topic) => {
-        fetch(`/${topic}`, {
-            method: 'PUT',
-            body: `This is a test notification for topic ${topic}!`
-        })
-    };
-
-    const showError = (msg) => {
-        errorField.innerHTML = msg;
-        topicField.disabled = true;
-        subscribeButton.disabled = true;
-    };
-
-    const showBrowserIncompatibleError = () => {
-        showError("Your browser is not compatible to use the web-based desktop notifications.");
-    };
-
-    const showNotificationDeniedError = () => {
-        showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications.");
-    };
-
-    subscribeForm.onsubmit = function () {
-        if (!topicField.value) {
-            return false;
-        }
-        subscribe(topicField.value);
-        topicField.value = "";
-        return false;
-    };
-
-    // Disable Web UI if notifications of EventSource are not available
-    if (!window["Notification"] || !window["EventSource"]) {
-        showBrowserIncompatibleError();
-    } else if (Notification.permission === "denied") {
-        showNotificationDeniedError();
-    }
-
-    // Reset UI
-    topicField.value = "";
-
-    // Restore topics
-    const storedTopics = localStorage.getItem('topics');
-    if (storedTopics && Notification.permission === "granted") {
-        const storedTopicsArray = JSON.parse(storedTopics)
-        storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); });
-        if (storedTopicsArray.length === 0) {
-            topicsHeader.style.display = 'none';
-        }
-    } else {
-        topicsHeader.style.display = 'none';
-    }
-</script>
-
+<script src="static/js/app.js"></script>
 </body>
 </html>

+ 12 - 0
server/server.go

@@ -2,6 +2,7 @@ package server
 
 import (
 	"bytes"
+	"embed"
 	_ "embed" // required for go:embed
 	"encoding/json"
 	"fmt"
@@ -51,10 +52,14 @@ var (
 	jsonRegex  = regexp.MustCompile(`^/[^/]+/json$`)
 	sseRegex   = regexp.MustCompile(`^/[^/]+/sse$`)
 	rawRegex   = regexp.MustCompile(`^/[^/]+/raw$`)
+	staticRegex   = regexp.MustCompile(`^/static/.+`)
 
 	//go:embed "index.html"
 	indexSource string
 
+	//go:embed static
+	webStaticFs embed.FS
+
 	errHTTPNotFound        = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)}
 	errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)}
 )
@@ -123,6 +128,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
 	}
 	if r.Method == http.MethodGet && r.URL.Path == "/" {
 		return s.handleHome(w, r)
+	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
+		return s.handleStatic(w, r)
 	} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
 		return s.handleSubscribeJSON(w, r)
 	} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
@@ -241,6 +248,11 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error {
 	return nil
 }
 
+func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
+	http.FileServer(http.FS(webStaticFs)).ServeHTTP(w, r)
+	return nil
+}
+
 func (s *Server) createTopic(id string) *topic {
 	s.mu.Lock()
 	defer s.mu.Unlock()

+ 76 - 0
server/static/css/app.css

@@ -0,0 +1,76 @@
+/* general styling */
+
+html, body {
+    font-family: 'Lato', sans-serif;
+    color: #333;
+    font-size: 1.1em;
+}
+
+a {
+    color: #39005a;
+}
+
+a:hover {
+    text-decoration: none;
+}
+
+h1 {
+    margin-top: 25px;
+    margin-bottom: 18px;
+    font-size: 2.5em;
+}
+
+h2 {
+    margin-top: 20px;
+    margin-bottom: 5px;
+    font-size: 1.8em;
+}
+
+h3 {
+    margin-top: 20px;
+    margin-bottom: 5px;
+    font-size: 1.3em;
+}
+
+p {
+    margin-top: 0;
+    font-size: 1.1em;
+}
+
+tt {
+    background: #eee;
+    padding: 2px 7px;
+    border-radius: 3px;
+}
+
+code {
+    display: block;
+    background: #eee;
+    font-family: monospace;
+    padding: 20px;
+    border-radius: 3px;
+}
+
+/* Lato font (OFL), https://fonts.google.com/specimen/Lato#about,
+   embedded with the help of https://google-webfonts-helper.herokuapp.com/fonts/lato?subsets=latin */
+
+@font-face {
+    font-family: 'Lato';
+    font-style: normal;
+    font-weight: 400;
+    src: local(''),
+    url('../font/lato-v17-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+    url('../font/lato-v17-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+
+/* Main page */
+
+#main {
+    max-width: 900px;
+    margin: 0 auto 50px auto;
+}
+
+#error {
+    color: darkred;
+    font-style: italic;
+}

BIN
server/static/font/lato-v17-latin-ext_latin-regular.woff


BIN
server/static/font/lato-v17-latin-ext_latin-regular.woff2


BIN
server/static/img/favicon.png


BIN
server/static/img/ntfy.png


+ 128 - 0
server/static/js/app.js

@@ -0,0 +1,128 @@
+
+/**
+ * Hello, dear curious visitor. I am not a web-guy, so please don't judge my horrible JS code.
+ * In fact, please do tell me about all the things I did wrong and that I could improve. I've been trying
+ * to read up on modern JS, but it's just a little much.
+ *
+ * Feel free to open tickets at https://github.com/binwiederhier/ntfy/issues. Thank you!
+ */
+
+/* All the things */
+
+let topics = {};
+
+const topicsHeader = document.getElementById("topicsHeader");
+const topicsList = document.getElementById("topicsList");
+const topicField = document.getElementById("topicField");
+const subscribeButton = document.getElementById("subscribeButton");
+const subscribeForm = document.getElementById("subscribeForm");
+const errorField = document.getElementById("error");
+
+const subscribe = (topic) => {
+    if (Notification.permission !== "granted") {
+        Notification.requestPermission().then((permission) => {
+            if (permission === "granted") {
+                subscribeInternal(topic, 0);
+            } else {
+                showNotificationDeniedError();
+            }
+        });
+    } else {
+        subscribeInternal(topic, 0);
+    }
+};
+
+const subscribeInternal = (topic, delaySec) => {
+    setTimeout(() => {
+        // Render list entry
+        let topicEntry = document.getElementById(`topic-${topic}`);
+        if (!topicEntry) {
+            topicEntry = document.createElement('li');
+            topicEntry.id = `topic-${topic}`;
+            topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
+            topicsList.appendChild(topicEntry);
+        }
+        topicsHeader.style.display = '';
+
+        // Open event source
+        let eventSource = new EventSource(`${topic}/sse`);
+        eventSource.onopen = () => {
+            topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
+            delaySec = 0; // Reset on successful connection
+        };
+        eventSource.onerror = (e) => {
+            const newDelaySec = (delaySec + 5 <= 15) ? delaySec + 5 : 15;
+            topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button disabled="disabled">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`;
+            eventSource.close()
+            subscribeInternal(topic, newDelaySec);
+        };
+        eventSource.onmessage = (e) => {
+            const event = JSON.parse(e.data);
+            new Notification(event.message);
+        };
+        topics[topic] = eventSource;
+        localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
+    }, delaySec * 1000);
+};
+
+const unsubscribe = (topic) => {
+    topics[topic].close();
+    delete topics[topic];
+    localStorage.setItem('topics', JSON.stringify(Object.keys(topics)));
+    document.getElementById(`topic-${topic}`).remove();
+    if (Object.keys(topics).length === 0) {
+        topicsHeader.style.display = 'none';
+    }
+};
+
+const test = (topic) => {
+    fetch(`/${topic}`, {
+        method: 'PUT',
+        body: `This is a test notification for topic ${topic}!`
+    })
+};
+
+const showError = (msg) => {
+    errorField.innerHTML = msg;
+    topicField.disabled = true;
+    subscribeButton.disabled = true;
+};
+
+const showBrowserIncompatibleError = () => {
+    showError("Your browser is not compatible to use the web-based desktop notifications.");
+};
+
+const showNotificationDeniedError = () => {
+    showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications.");
+};
+
+subscribeForm.onsubmit = function () {
+    if (!topicField.value) {
+        return false;
+    }
+    subscribe(topicField.value);
+    topicField.value = "";
+    return false;
+};
+
+// Disable Web UI if notifications of EventSource are not available
+if (!window["Notification"] || !window["EventSource"]) {
+    showBrowserIncompatibleError();
+} else if (Notification.permission === "denied") {
+    showNotificationDeniedError();
+}
+
+// Reset UI
+topicField.value = "";
+
+// Restore topics
+const storedTopics = localStorage.getItem('topics');
+if (storedTopics && Notification.permission === "granted") {
+    const storedTopicsArray = JSON.parse(storedTopics)
+    storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); });
+    if (storedTopicsArray.length === 0) {
+        topicsHeader.style.display = 'none';
+    }
+} else {
+    topicsHeader.style.display = 'none';
+}