|
@@ -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">
|
|
|
+
|
|
|
+
|
|
|
+ <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">
|
|
|
+
|
|
|
+
|
|
|
+ <meta name="theme-color" content="#004c79">
|
|
|
+ <meta name="msapplication-navbutton-color" content="#004c79">
|
|
|
+ <meta name="apple-mobile-web-app-status-bar-style" content="#004c79">
|
|
|
+
|
|
|
+
|
|
|
+ <link rel="icon" type="image/png" href="static/img/favicon.png">
|
|
|
+
|
|
|
+
|
|
|
+ <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>
|