Skip to content

Commit

Permalink
notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinDFuller committed Aug 12, 2023
1 parent a12ef64 commit ac66e88
Show file tree
Hide file tree
Showing 7 changed files with 689 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .appengine/app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ service: default

automatic_scaling:
max_instances: 1
max_idle_instances: 1
max_idle_instances: 0

handlers:
- url: /image
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export GOOGLE_CLOUD_PROJECT=justindfuller
export GAE_DEPLOYMENT_ID=localhost/$(shell date --iso=seconds)
export PORT=9000

Expand Down
26 changes: 26 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,33 @@ require golang.org/x/sync v0.2.0
require github.com/yuin/goldmark v1.5.4

require (
cloud.google.com/go/cloudtasks v1.12.1 // indirect
cloud.google.com/go/compute v1.19.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/secretmanager v1.10.0 // indirect
github.com/SherClockHolmes/webpush-go v1.2.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
github.com/justindfuller/secretmanager v0.0.4 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/api v0.126.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)
501 changes: 501 additions & 0 deletions go.sum

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
"text/template"
"time"

cloudtasks "cloud.google.com/go/cloudtasks/apiv2"
taskspb "cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb"
webpush "github.com/SherClockHolmes/webpush-go"
"github.com/justindfuller/justindfuller.com/aphorism"
"github.com/justindfuller/justindfuller.com/poem"
"github.com/justindfuller/justindfuller.com/review"
"github.com/justindfuller/justindfuller.com/story"
"github.com/justindfuller/justindfuller.com/word"
"github.com/justindfuller/secretmanager"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/timestamppb"
)

type data struct {
Expand All @@ -24,7 +31,26 @@ type data struct {
Entry []byte
}

type Reminder struct {
Time time.Time `json:"time"`
Minutes int `json:"minutes"`
Subscription *webpush.Subscription `json:"subscription"`
}

type ReminderConfig struct {
PublicKey string `secretmanager:"reminder_public_key"`
PrivateKey string `secretmanager:"reminder_private_key"`
Subscriber string `secretmanager:"reminder_subscriber"`
}

func main() {
var reminderConfig ReminderConfig
if err := secretmanager.Parse(&reminderConfig); err != nil {
log.Fatalf("Error reading secrets: %s", err)

return
}

http.HandleFunc("/aphorism", func(w http.ResponseWriter, r *http.Request) {
entries, err := aphorism.Entries()
if err != nil {
Expand Down Expand Up @@ -179,6 +205,106 @@ func main() {
http.ServeFile(w, r, fmt.Sprintf(".%s", r.URL.Path))
})

http.HandleFunc("/reminder/set", func(w http.ResponseWriter, r *http.Request) {
client, err := cloudtasks.NewClient(r.Context())
if err != nil {
log.Printf("Error creating cloud task client: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}
defer client.Close()

var reminder Reminder
if err := json.NewDecoder(r.Body).Decode(&reminder); err != nil {
log.Printf("Error decoding body: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

if err := r.Body.Close(); err != nil {
log.Printf("Error closing request body: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

task := &taskspb.CreateTaskRequest{
Parent: "projects/justindfuller/locations/us-central1/queues/grass",
Task: &taskspb.Task{
// https://godoc.org/google.golang.org/genproto/googleapis/cloud/tasks/v2#AppEngineHttpRequest
ScheduleTime: timestamppb.New(reminder.Time),
MessageType: &taskspb.Task_AppEngineHttpRequest{
AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{
HttpMethod: taskspb.HttpMethod_POST,
RelativeUri: "/reminder/send",
},
},
},
}

body, err := json.Marshal(reminder)
if err != nil {
log.Printf("Error encoding reminder: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

task.Task.GetAppEngineHttpRequest().Body = body

createdTask, err := client.CreateTask(r.Context(), task)
if err != nil {
log.Printf("Error creating reminder: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

log.Printf("Created task: %v", createdTask)
})

http.HandleFunc("/reminder/send", func(w http.ResponseWriter, r *http.Request) {
var reminder Reminder
if err := json.NewDecoder(r.Body).Decode(&reminder); err != nil {
log.Printf("Error decoding body: %s", err)
http.Error(w, "Error sending reminder", http.StatusInternalServerError)

return
}

body, err := json.Marshal(reminder)
if err != nil {
log.Printf("Error encoding body: %s", err)
http.Error(w, "Error sending reminder", http.StatusInternalServerError)

return
}

resp, err := webpush.SendNotification(body, reminder.Subscription, &webpush.Options{
Subscriber: reminderConfig.Subscriber,
VAPIDPublicKey: reminderConfig.PublicKey,
VAPIDPrivateKey: reminderConfig.PrivateKey,
TTL: 1000 * 60 * 60 * 12, // 12 hours
})
if err != nil {
log.Printf("Error sending push notification: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

if err := resp.Body.Close(); err != nil {
log.Printf("Error closing response body: %s", err)
http.Error(w, "Error creating reminder", http.StatusInternalServerError)

return
}

log.Printf("Sent push notification: %v", r)
})

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := template.Must(template.ParseFiles("./main.template.html", "./main.js", "./main.css", "./meta.template.html")).Execute(w, data{}); err != nil {
log.Printf("Error: %s", err)
Expand Down
36 changes: 31 additions & 5 deletions make/grass.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,14 @@
const grass = grassTypes.find((g) => g.name === target.value);

window.localStorage.setItem("grass", grass.name);
const deficiency =
forecast.deficiency =
Math.round((grass.inches - forecast.totalInches) * 100) / 100;
const third = Math.round((deficiency / 3) * 100) / 100;
forecast.third = Math.round((forecast.deficiency / 3) * 100) / 100;
forecast.minutesEachDay = Math.round(60 * forecast.third);

wateringNeedsAmount.innerText = grass.inches;
wateringMinutesEachDay.innerText = Math.round(60 * third);
wateringDeficiency.innerText = deficiency;
wateringMinutesEachDay.innerText = forecast.minutesEachDay;
wateringDeficiency.innerText = forecast.deficiency;
wateringNeeds.classList.remove("hidden");

const sorted = Object.values(forecast.days).sort((a, b) => {
Expand Down Expand Up @@ -273,6 +274,8 @@
}

async function handleReminderClick() {
document.getElementById("notifications").classList.add("hidden");

const reg = await navigator.serviceWorker.getRegistration("/grass/service-worker.js");
if (!reg) {
alert("Unable to set up notifications.")
Expand All @@ -292,7 +295,30 @@
userVisibleOnly: true,
applicationServerKey: "BMhhlc_OBTiPkzt6sYneuv_kWlgWATUFANJr5x1PBWpT7eMeVHLcW-oIzhOrZiiTGRITeqGVAphu1dGEpT_tYG0",
}).then((subscription) => {
console.log({ subscription })

for (const date in forecast.days) {
const day = forecast.days[date];

if (day.willWater === true) {
const enc = new TextDecoder("utf-8");

const body = {
time: new Date(date),
minutes: forecast.minutesEachDay,
subscription: subscription.toJSON(),
};

const stringified = JSON.stringify(body);

console.log("Setting reminder for", { body, stringified })

fetch("/reminder/set", {
method: "POST",
body: stringified,
})
}
}

});
}
}).catch((e) => {
Expand Down
6 changes: 3 additions & 3 deletions make/grass.worker.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
self.addEventListener('install', event => console.log('ServiceWorker installed'));

self.addEventListener('push', function(event) {
console.log({ event });
const data = event?.data?.json();

event.waitUntil(self.registration.showNotification("Grass | Justin Fuller", {
body: "Today's the day. Water your lawn!",
event.waitUntil(self.registration.showNotification("Remember to water your lawn!", {
body: data?.minutes ? `Water for ${data?.minutes} minutes. Click to see your schedule.` : "Click to see today's watering times.",
icon: "/image/grass.png",
}));
})
Expand Down

0 comments on commit ac66e88

Please sign in to comment.