implement unregister
This commit is contained in:
94
main.go
94
main.go
@@ -90,7 +90,7 @@ var supportedLocations = map[string]location{
|
|||||||
func main() {
|
func main() {
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
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()
|
db, err := initDB()
|
||||||
@@ -164,10 +164,17 @@ func main() {
|
|||||||
s.Start()
|
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.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 {
|
for _, s := range schedulers {
|
||||||
s.Shutdown()
|
s.Shutdown()
|
||||||
@@ -212,7 +219,7 @@ func handleHTTPRequest(state *state) http.HandlerFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
} else if request.Method == "PATCH" {
|
} else if request.Method == "PATCH" || request.Method == "DELETE" {
|
||||||
parts := strings.Split(path, "/")
|
parts := strings.Split(path, "/")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
@@ -225,26 +232,41 @@ func handleHTTPRequest(state *state) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer request.Body.Close()
|
switch request.Method {
|
||||||
|
case "PATCH":
|
||||||
|
defer request.Body.Close()
|
||||||
|
|
||||||
update := updateSubscription{}
|
update := updateSubscription{}
|
||||||
err = json.NewDecoder(request.Body).Decode(&update)
|
err = json.NewDecoder(request.Body).Decode(&update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reg, err := updateRegisteredSubscription(state, regID, &update)
|
reg, err := updateRegisteredSubscription(state, regID, &update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
} else {
|
} else {
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
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 {
|
} else {
|
||||||
writer.WriteHeader(http.StatusMethodNotAllowed)
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
@@ -261,7 +283,7 @@ func handleHTTPRequest(state *state) http.HandlerFunc {
|
|||||||
} else {
|
} else {
|
||||||
f, err := webDir.ReadFile("web/" + path)
|
f, err := webDir.ReadFile("web/" + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(404)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
} else {
|
} else {
|
||||||
m := mime.TypeByExtension(filepath.Ext(path))
|
m := mime.TypeByExtension(filepath.Ext(path))
|
||||||
if m != "" {
|
if m != "" {
|
||||||
@@ -383,6 +405,11 @@ func registerSubscription(state *state, sub *updateSubscription) (*registeredSub
|
|||||||
return ®, nil
|
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) {
|
func updateSummaries(state *state, locKey string, loc *location) {
|
||||||
log.Printf("updating summary for %v...\n", locKey)
|
log.Printf("updating summary for %v...\n", locKey)
|
||||||
|
|
||||||
@@ -427,17 +454,26 @@ func listenForSummaryUpdates(state *state, locKey string) {
|
|||||||
select {
|
select {
|
||||||
case summary := <-c:
|
case summary := <-c:
|
||||||
log.Printf("sending summary for %v to subscribers...\n", locKey)
|
log.Printf("sending summary for %v to subscribers...\n", locKey)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
for _, sub := range state.subscriptions[locKey] {
|
for _, sub := range state.subscriptions[locKey] {
|
||||||
_, err := webpush.SendNotificationWithContext(state.ctx, []byte(summary), sub.Subscription, &webpush.Options{
|
wg.Add(1)
|
||||||
VAPIDPublicKey: state.vapidPublicKey,
|
go func() {
|
||||||
VAPIDPrivateKey: state.vapidPrivateKey,
|
defer wg.Done()
|
||||||
TTL: 30,
|
|
||||||
})
|
_, err := webpush.SendNotificationWithContext(state.ctx, []byte(summary), sub.Subscription, &webpush.Options{
|
||||||
if err != nil {
|
VAPIDPublicKey: state.vapidPublicKey,
|
||||||
log.Printf("failed to send notification %e\n", err)
|
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():
|
case <-state.ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -2,27 +2,30 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>7am</title>
|
<title>7am Weather</title>
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
||||||
<link href="./style.css" rel="stylesheet">
|
<link href="./style.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>7am</h1>
|
<h1>7am</h1>
|
||||||
<h2>Daily weather updates delivered to you at 7am.</h2>
|
<h2>Daily weather updates delivered to you at 7am.</h2>
|
||||||
<hr class="divider" />
|
<hr class="divider" />
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/london">London</a></li>
|
<li><a href="/london">London</a></li>
|
||||||
<li><a href="/sf">San Francisco</a></li>
|
<li><a href="/sf">San Francisco</a></li>
|
||||||
<li><a href="/sj">San Jose</a></li>
|
<li><a href="/sj">San Jose</a></li>
|
||||||
<li><a href="/la">Los Angeles</a></li>
|
<li><a href="/la">Los Angeles</a></li>
|
||||||
<li><a href="/nyc">New York City</a></li>
|
<li><a href="/nyc">New York City</a></li>
|
||||||
<li><a href="/tokyo">Tokyo</a></li>
|
<li><a href="/tokyo">Tokyo</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
html, body {
|
html, body {
|
||||||
font-family: Geist, sans-serif;
|
font-family: Geist, sans-serif;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
padding-top: 4rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@@ -2,11 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>WeatherBoy</title>
|
<title>7am Weather</title>
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
|
||||||
<link href="/style.css" rel="stylesheet">
|
<link href="/style.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
const KEY_SUBSCRIPTION = "subscription"
|
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 getSummaryButton = document.getElementById("get-summary-btn")
|
||||||
const loc = getSummaryButton.dataset.loc
|
const loc = getSummaryButton.dataset.loc
|
||||||
getSummaryButton.style.display = "none"
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
getSummaryButton.style.display = "none"
|
||||||
|
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker.register("/sw.js")
|
navigator.serviceWorker.register("/sw.js")
|
||||||
})
|
})
|
||||||
@@ -29,13 +30,33 @@ async function main() {
|
|||||||
async function onButtonClick() {
|
async function onButtonClick() {
|
||||||
const reg = await navigator.serviceWorker.ready
|
const reg = await navigator.serviceWorker.ready
|
||||||
|
|
||||||
|
const pushSub = await reg.pushManager.getSubscription()
|
||||||
const existingSubscriptionJson = localStorage.getItem(KEY_SUBSCRIPTION)
|
const existingSubscriptionJson = localStorage.getItem(KEY_SUBSCRIPTION)
|
||||||
const existingSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null
|
const registeredSubscription = existingSubscriptionJson ? JSON.parse(existingSubscriptionJson) : null
|
||||||
const currentlyEnabled = existingSubscription?.locations?.includes(loc) ?? false
|
const currentlyEnabled = (registeredSubscription?.locations?.includes(loc) ?? false) && pushSub !== null
|
||||||
|
|
||||||
if (currentlyEnabled) {
|
if (currentlyEnabled) {
|
||||||
await reg.pushManager.getSubscription().then((sub) => sub?.unsubscribe())
|
registeredSubscription.locations.splice(
|
||||||
localStorage.removeItem(KEY_SUBSCRIPTION)
|
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"
|
getSummaryButton.innerText = "Get daily updates at 7am"
|
||||||
} else {
|
} else {
|
||||||
const worker = await navigator.serviceWorker.ready
|
const worker = await navigator.serviceWorker.ready
|
||||||
@@ -50,27 +71,22 @@ async function onButtonClick() {
|
|||||||
const pushSub = await worker.pushManager.subscribe({
|
const pushSub = await worker.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: publicKey
|
applicationServerKey: publicKey
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
registeredSubscription.locations.push(loc)
|
||||||
|
|
||||||
let newSubscription
|
let newSubscription
|
||||||
if (existingSubscription) {
|
if (registeredSubscription) {
|
||||||
newSubscription = await fetch(`/registrations/${existingSubscription.id}`, {
|
newSubscription = await fetch(`/registrations/${registeredSubscription.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
subscription: pushSub,
|
subscription: pushSub,
|
||||||
locations: [...existingSubscription.locations, loc]
|
locations: registeredSubscription.locations,
|
||||||
})
|
})
|
||||||
}).then((res) => {
|
}).then(jsonOrThrow)
|
||||||
if (res.status === 200) {
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
throw new Error(`${res.status}`)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
newSubscription = await fetch("/registrations", {
|
newSubscription = await fetch("/registrations", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -81,22 +97,23 @@ async function onButtonClick() {
|
|||||||
subscription: pushSub,
|
subscription: pushSub,
|
||||||
locations: [loc]
|
locations: [loc]
|
||||||
})
|
})
|
||||||
}).then((res) => {
|
}).then(jsonOrThrow)
|
||||||
if (res.status === 200) {
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
throw new Error(`${res.status}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(KEY_SUBSCRIPTION, JSON.stringify(newSubscription))
|
localStorage.setItem(KEY_SUBSCRIPTION, JSON.stringify(newSubscription))
|
||||||
|
|
||||||
getSummaryButton.innerText = "Stop updates"
|
getSummaryButton.innerText = "Stop updates"
|
||||||
} catch (error) {
|
} 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) {
|
if (canReceiveUpdates) {
|
||||||
|
Reference in New Issue
Block a user