diff --git a/server/index.html b/server/index.html index 9ab3b9b9dde459546d52f933e47b6b9337dc6d93..eaa4d5f0f82fe1d1c42e4ff6054f52ae2515c2b9 100644 --- a/server/index.html +++ b/server/index.html @@ -8,71 +8,89 @@ <body> <h1>ntfy.sh</h1> -Topics: -<ul id="topics"> -</ul> +<p> + ntfy.sh is a super simple pub-sub notification service. It allows you to send desktop and (soon) phone notifications + via scripts, without signup or cost. It's entirely free and open source. +</p> -<input type="text" id="topic" size="64" autofocus /> -<button id="topicButton">Add topic</button> -<button onclick="notifyMe('test'); return false">Notify me!</button> +<p> + <b>Usage:</b> You can subscribe to a topic either in this web UI, or in your own app by subscribing to an SSE/EventSource + or JSON feed. Once subscribed, you can publish messages via PUT or POST. +</p> <div id="error"></div> -<script type="text/javascript"> - window.onload = function() { - let topics = {}; - const topicField = document.getElementById("topic"); - const topicsList = document.getElementById("topics"); - const topicButton = document.getElementById("topicButton"); - const errorField = document.getElementById("error"); +<form id="subscribeForm"> + <input type="text" id="topicField" size="64" autofocus /> + <input type="submit" id="subscribeButton" value="Subscribe topic" /> +</form> - const subscribe = function (topic) { - let conn = new WebSocket(`ws://${document.location.host}/${topic}/ws`); - conn.onclose = function (evt) { - errorField.innerHTML = "Connection closed"; - }; - conn.onmessage = function (evt) { - notify(evt.data) - }; - topics[topic] = conn; +Topics: +<ul id="topicsList"> +</ul> - let topicEntry = document.createElement('li'); - topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; - topicsList.appendChild(topicEntry); - }; +<script type="text/javascript"> + let topics = {}; - const notify = function (msg) { - if (!("Notification" in window)) { - alert("This browser does not support desktop notification"); - } else if (Notification.permission === "granted") { - var notification = new Notification(msg); - } else if (Notification.permission !== "denied") { - Notification.requestPermission().then(function (permission) { - if (permission === "granted") { - var notification = new Notification(msg); - } - }); - } + const topicField = document.getElementById("topicField"); + const topicsList = document.getElementById("topicsList"); + const subscribeButton = document.getElementById("subscribeButton"); + const subscribeForm = document.getElementById("subscribeForm"); + const errorField = document.getElementById("error"); + + const subscribe = function (topic) { + if (Notification.permission !== "granted") { + Notification.requestPermission().then(function (permission) { + if (permission === "granted") { + subscribeInternal(topic); + } + }); + } else { + subscribeInternal(topic); } + }; - topicButton.onclick = function () { - if (!topicField.value) { - return false; - } - subscribe(topicField.value); - return false; + const subscribeInternal = function (topic) { + let eventSource = new EventSource(`${topic}/sse`); + eventSource.onerror = function (e) { + console.log(e); + errorField.innerHTML = "Error " + e; + }; + eventSource.onmessage = function (e) { + const event = JSON.parse(e.data); + new Notification(event.message); }; + topics[topic] = eventSource; + + let topicEntry = document.createElement('li'); + topicEntry.id = `topic-${topic}`; + topicEntry.innerHTML = `${topic} <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; + topicsList.appendChild(topicEntry); + }; - if (!window["Notification"]) { - errorField.innerHTML = "Your browser does not support desktop notifications"; - topicField.disabled = true; - topicButton.disabled = true; - } else if (!window["Notification"]) { - errorField.innerHTML = "Your browser does not support WebSockets."; - topicField.disabled = true; - topicButton.disabled = true; + const unsubscribe = function(topic) { + topics[topic].close(); + document.getElementById(`topic-${topic}`).remove(); + }; + + subscribeForm.onsubmit = function () { + alert("hi") + if (!topicField.value) { + return false; } + subscribe(topicField.value); + return false; }; + + if (!window["Notification"] || !window["EventSource"]) { + errorField.innerHTML = "Your browser is not compatible to use the web-based desktop notifications."; + topicField.disabled = true; + subscribeButton.disabled = true; + } else if (Notification.permission === "denied") { + errorField.innerHTML = "You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications."; + topicField.disabled = true; + subscribeButton.disabled = true; + } </script> </body> diff --git a/server/server.go b/server/server.go index 3b839b820c6fa0558098944d337c659fd785c0de..a78019c50dd3bc463224cf2e71f63ee6ebfeee66 100644 --- a/server/server.go +++ b/server/server.go @@ -5,6 +5,7 @@ import ( _ "embed" // required for go:embed "encoding/json" "errors" + "fmt" "github.com/gorilla/websocket" "io" "log" @@ -31,8 +32,9 @@ const ( var ( topicRegex = regexp.MustCompile(`^/[^/]+$`) - wsRegex = regexp.MustCompile(`^/[^/]+/ws$`) jsonRegex = regexp.MustCompile(`^/[^/]+/json$`) + sseRegex = regexp.MustCompile(`^/[^/]+/sse$`) + wsRegex = regexp.MustCompile(`^/[^/]+/ws$`) wsUpgrader = websocket.Upgrader{ ReadBufferSize: messageLimit, WriteBufferSize: messageLimit, @@ -82,7 +84,9 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { } else if r.Method == http.MethodGet && wsRegex.MatchString(r.URL.Path) { return s.handleSubscribeWS(w, r) } else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) { - return s.handleSubscribeHTTP(w, r) + return s.handleSubscribeJSON(w, r) + } else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) { + return s.handleSubscribeSSE(w, r) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) { return s.handlePublishHTTP(w, r) } @@ -112,7 +116,7 @@ func (s *Server) handlePublishHTTP(w http.ResponseWriter, r *http.Request) error return t.Publish(msg) } -func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request) error { +func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request) error { t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/json")) // Hack subscriberID := t.Subscribe(func (msg *message) error { if err := json.NewEncoder(w).Encode(&msg); err != nil { @@ -131,6 +135,32 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request) err return nil } +func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request) error { + t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/sse")) // Hack + subscriberID := t.Subscribe(func (msg *message) error { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(&msg); err != nil { + return err + } + m := fmt.Sprintf("data: %s\n\n", buf.String()) + if _, err := io.WriteString(w, m); err != nil { + return err + } + if fl, ok := w.(http.Flusher); ok { + fl.Flush() + } + return nil + }) + defer t.Unsubscribe(subscriberID) + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + select { + case <-t.ctx.Done(): + case <-r.Context().Done(): + } + return nil +} + func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request) error { conn, err := wsUpgrader.Upgrade(w, r, nil) if err != nil {