From 56bc155aa58eb6819686b26ddcabc7eb3ec0c0a5 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sat, 10 May 2025 14:58:41 +0100 Subject: [PATCH] implement unregister --- main.go | 94 +++++++++++++++++++++++++++++++++--------------- web/index.html | 31 ++++++++-------- web/style.css | 2 +- web/summary.html | 5 ++- web/summary.js | 65 ++++++++++++++++++++------------- 5 files changed, 128 insertions(+), 69 deletions(-) diff --git a/main.go b/main.go index bf2b9a8..4ae9afa 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,7 @@ var supportedLocations = map[string]location{ func main() { err := godotenv.Load() if err != nil { - log.Fatalln("Please create a .env file using the provided template!") + log.Fatalln("please create a .env file using the provided template!") } db, err := initDB() @@ -164,10 +164,17 @@ func main() { s.Start() } - loadSubscriptions(&state) + err = loadSubscriptions(&state) + if err != nil { + log.Fatalf("failed to load existing subscriptions: %e\n", err) + } http.HandleFunc("/", handleHTTPRequest(&state)) - http.ListenAndServe(":8080", nil) + err = http.ListenAndServe(":8080", nil) + + if err != nil { + log.Printf("failed to start http server: %e\n", err) + } for _, s := range schedulers { s.Shutdown() @@ -212,7 +219,7 @@ func handleHTTPRequest(state *state) http.HandlerFunc { if err != nil { writer.WriteHeader(http.StatusBadRequest) } - } else if request.Method == "PATCH" { + } else if request.Method == "PATCH" || request.Method == "DELETE" { parts := strings.Split(path, "/") if len(parts) < 2 { writer.WriteHeader(http.StatusMethodNotAllowed) @@ -225,26 +232,41 @@ func handleHTTPRequest(state *state) http.HandlerFunc { return } - defer request.Body.Close() + switch request.Method { + case "PATCH": + defer request.Body.Close() - update := updateSubscription{} - err = json.NewDecoder(request.Body).Decode(&update) - if err != nil { - writer.WriteHeader(http.StatusBadRequest) - return - } - - reg, err := updateRegisteredSubscription(state, regID, &update) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - writer.WriteHeader(http.StatusNotFound) - } else { - writer.WriteHeader(http.StatusInternalServerError) + update := updateSubscription{} + err = json.NewDecoder(request.Body).Decode(&update) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return + } + + reg, err := updateRegisteredSubscription(state, regID, &update) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + writer.WriteHeader(http.StatusNotFound) + } else { + writer.WriteHeader(http.StatusInternalServerError) + } + } else { + json.NewEncoder(writer).Encode(reg) + } + + case "DELETE": + err = deleteSubscription(state, regID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + writer.WriteHeader(http.StatusNotFound) + } else { + writer.WriteHeader(http.StatusInternalServerError) + } + } else { + writer.WriteHeader(http.StatusNoContent) } - return } - json.NewEncoder(writer).Encode(reg) } else { writer.WriteHeader(http.StatusMethodNotAllowed) } @@ -261,7 +283,7 @@ func handleHTTPRequest(state *state) http.HandlerFunc { } else { f, err := webDir.ReadFile("web/" + path) if err != nil { - writer.WriteHeader(404) + writer.WriteHeader(http.StatusNotFound) } else { m := mime.TypeByExtension(filepath.Ext(path)) if m != "" { @@ -383,6 +405,11 @@ func registerSubscription(state *state, sub *updateSubscription) (*registeredSub return ®, nil } +func deleteSubscription(state *state, regID uuid.UUID) error { + _, err := state.db.Exec("DELETE FROM subscriptions WHERE id = ?", regID) + return err +} + func updateSummaries(state *state, locKey string, loc *location) { log.Printf("updating summary for %v...\n", locKey) @@ -427,17 +454,26 @@ func listenForSummaryUpdates(state *state, locKey string) { select { case summary := <-c: log.Printf("sending summary for %v to subscribers...\n", locKey) + + var wg sync.WaitGroup for _, sub := range state.subscriptions[locKey] { - _, err := webpush.SendNotificationWithContext(state.ctx, []byte(summary), sub.Subscription, &webpush.Options{ - VAPIDPublicKey: state.vapidPublicKey, - VAPIDPrivateKey: state.vapidPrivateKey, - TTL: 30, - }) - if err != nil { - log.Printf("failed to send notification %e\n", err) - } + wg.Add(1) + go func() { + defer wg.Done() + + _, err := webpush.SendNotificationWithContext(state.ctx, []byte(summary), sub.Subscription, &webpush.Options{ + VAPIDPublicKey: state.vapidPublicKey, + VAPIDPrivateKey: state.vapidPrivateKey, + TTL: 30, + }) + if err != nil { + log.Printf("failed to send summary for %v to sub id %v: %e\n", locKey, sub.ID, err) + } + }() } + wg.Wait() + case <-state.ctx.Done(): return } diff --git a/web/index.html b/web/index.html index 47177af..4f2911d 100644 --- a/web/index.html +++ b/web/index.html @@ -2,27 +2,30 @@ - 7am + 7am Weather + + + -
-

7am

-

Daily weather updates delivered to you at 7am.

-
- -
+
+

7am

+

Daily weather updates delivered to you at 7am.

+
+ +
diff --git a/web/style.css b/web/style.css index eb06773..4f35ee9 100644 --- a/web/style.css +++ b/web/style.css @@ -17,7 +17,7 @@ html, body { font-family: Geist, sans-serif; width: 100%; - height: 100%; + padding-top: 4rem; display: flex; align-items: center; justify-content: center; diff --git a/web/summary.html b/web/summary.html index 5492442..64a4d2d 100644 --- a/web/summary.html +++ b/web/summary.html @@ -2,11 +2,14 @@ - WeatherBoy + 7am Weather + + + diff --git a/web/summary.js b/web/summary.js index 3caaca2..463e9cc 100644 --- a/web/summary.js +++ b/web/summary.js @@ -1,11 +1,12 @@ const KEY_SUBSCRIPTION = "subscription" -const canReceiveUpdates = "Notification" in window && "serviceWorker" in navigator +const canReceiveUpdates = "serviceWorker" in navigator const getSummaryButton = document.getElementById("get-summary-btn") const loc = getSummaryButton.dataset.loc -getSummaryButton.style.display = "none" async function main() { + getSummaryButton.style.display = "none" + window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js") }) @@ -29,13 +30,33 @@ async function main() { async function onButtonClick() { const reg = await navigator.serviceWorker.ready + const pushSub = await reg.pushManager.getSubscription() const existingSubscriptionJson = localStorage.getItem(KEY_SUBSCRIPTION) - const existingSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null - const currentlyEnabled = existingSubscription?.locations?.includes(loc) ?? false + const registeredSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null + const currentlyEnabled = (registeredSubscription?.locations?.includes(loc) ?? false) && pushSub !== null if (currentlyEnabled) { - await reg.pushManager.getSubscription().then((sub) => sub?.unsubscribe()) - localStorage.removeItem(KEY_SUBSCRIPTION) + registeredSubscription.locations.splice( + registeredSubscription.locations.indexOf(loc), + 1 + ) + if (registeredSubscription.locations.length === 0) { + await reg.pushManager.getSubscription().then((sub) => sub?.unsubscribe()) + await fetch(`/registrations/${registeredSubscription.id}`, { method: "DELETE" }) + localStorage.removeItem(KEY_SUBSCRIPTION) + } else { + const newReg = await fetch(`/registrations/${registeredSubscription.id}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + subscription: pushSub, + locations: registeredSubscription.locations, + }) + }).then(jsonOrThrow) + localStorage.setItem(KEY_SUBSCRIPTION, newReg) + } getSummaryButton.innerText = "Get daily updates at 7am" } else { const worker = await navigator.serviceWorker.ready @@ -50,27 +71,22 @@ async function onButtonClick() { const pushSub = await worker.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey - }).catch((error) => { - console.error(error) }) + registeredSubscription.locations.push(loc) + let newSubscription - if (existingSubscription) { - newSubscription = await fetch(`/registrations/${existingSubscription.id}`, { + if (registeredSubscription) { + newSubscription = await fetch(`/registrations/${registeredSubscription.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ subscription: pushSub, - locations: [...existingSubscription.locations, loc] + locations: registeredSubscription.locations, }) - }).then((res) => { - if (res.status === 200) { - return res.json() - } - throw new Error(`${res.status}`) - }) + }).then(jsonOrThrow) } else { newSubscription = await fetch("/registrations", { method: "POST", @@ -81,22 +97,23 @@ async function onButtonClick() { subscription: pushSub, locations: [loc] }) - }).then((res) => { - if (res.status === 200) { - return res.json() - } - throw new Error(`${res.status}`) - }) + }).then(jsonOrThrow) } localStorage.setItem(KEY_SUBSCRIPTION, JSON.stringify(newSubscription)) getSummaryButton.innerText = "Stop updates" } catch (error) { - console.log(error) + alert(`Error when trying to subscribe to updates: ${error}`) } } +} +function jsonOrThrow(res) { + if (res.status === 200) { + return res.json() + } + throw new Error(`server returned status ${res.status}`) } if (canReceiveUpdates) {