From 7ccd1a9effd63fc83f75654f1e0f822cdf0435e9 Mon Sep 17 00:00:00 2001 From: collin Date: Sun, 22 Jun 2025 21:59:09 +0200 Subject: [PATCH] Adding basic backend to handle RSVPs, still a WIP --- server/database.go | 28 ++++++++++ server/go.mod | 9 ++++ server/go.sum | 6 +++ server/http.go | 43 +++++++++++++++ server/main.go | 14 +++++ server/ntfy.go | 67 +++++++++++++++++++++++ server/rsvp.go | 129 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 296 insertions(+) create mode 100644 server/database.go create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/http.go create mode 100644 server/main.go create mode 100644 server/ntfy.go create mode 100644 server/rsvp.go diff --git a/server/database.go b/server/database.go new file mode 100644 index 0000000..b9ee562 --- /dev/null +++ b/server/database.go @@ -0,0 +1,28 @@ +package main + +import ( + "database/sql" + "fmt" + "log" +) + +func SetupDatabase() *sql.DB { + db, err := sql.Open("sqlite3", "./db.sqlite") + + if err != nil { + log.Fatal("failed to open database") + } + + var version string + err = db.QueryRow("SELECT SQLITE_VERSION()").Scan(&version) + + if err != nil { + log.Fatal("failed to query database") + } + + fmt.Println(version) + + SetupRsvpsTable(db) + + return db +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..cb9a734 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,9 @@ +module collinenlucy.nl + +go 1.24.4 + +require ( + github.com/AnthonyHewins/gotfy v0.0.10 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..8863dcf --- /dev/null +++ b/server/go.sum @@ -0,0 +1,6 @@ +github.com/AnthonyHewins/gotfy v0.0.10 h1:23ZjRVG7wuGuqn7CQq/bOrkXa3gg2XyYrrD3RYEmvE8= +github.com/AnthonyHewins/gotfy v0.0.10/go.mod h1:q2orErDDpl9/gZ5L4oJhejb7TaP/eBdtkzjWDruNRlg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/server/http.go b/server/http.go new file mode 100644 index 0000000..2c133a5 --- /dev/null +++ b/server/http.go @@ -0,0 +1,43 @@ +package main + +import ( + "database/sql" + "fmt" + "net/http" + "time" +) + +type Handler struct { + db *sql.DB +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fmt.Printf("%s - [%s] (%s) %s\n", time.Now().Format(time.RFC3339), r.RemoteAddr, r.Method, r.URL) + + switch true { + case r.Method == "GET" && r.URL.Path == "/": + rsvps, err := GetRsvps(h.db) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "%s", err.Error()) + return + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "%#v", rsvps) + case r.Method == "POST" && r.URL.Path == "/": + r := Rsvp{ + id: -1, + attending: true, + partySize: 2, + partyMembers: []Member{ + {name: "test", dietaryPreferences: "no meat", child: false}, + {name: "; DROP TABLE rsvps", dietaryPreferences: "", child: true}, + }, + } + r.CreateRsvp(h.db) + w.WriteHeader(http.StatusAccepted) + default: + w.WriteHeader(http.StatusNotFound) + } +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..de5e9b3 --- /dev/null +++ b/server/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "net/http" + + _ "github.com/mattn/go-sqlite3" +) + +func main() { + SetupNtfyClient() + db := SetupDatabase() + hnd := &Handler{db} + http.ListenAndServe(":8000", hnd) +} diff --git a/server/ntfy.go b/server/ntfy.go new file mode 100644 index 0000000..dcdd311 --- /dev/null +++ b/server/ntfy.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/AnthonyHewins/gotfy" +) + +var client *gotfy.Publisher + +type authdTransport struct { + underlyingTransport http.RoundTripper +} + +func (t *authdTransport) RoundTrip(req *http.Request) (*http.Response, error) { + var ntfyToken string + env := os.Environ() + for _, keyVal := range env { + keyValArr := strings.Split(keyVal, "=") + if keyValArr[0] == "NTFY_TOKEN" { + ntfyToken = keyValArr[1] + } + } + + req.Header.Add("Authorization", "Bearer "+ntfyToken) + return t.underlyingTransport.RoundTrip(req) +} + +func SetupNtfyClient() { + server, err := url.Parse("https://home.collinduncan.com/ntfy") + + if err != nil { + log.Fatal(err) + } + + authdHttpClient := &http.Client{Transport: &authdTransport{underlyingTransport: http.DefaultTransport}} + + tp, err := gotfy.NewPublisher(server, authdHttpClient) + + if err != nil { + log.Fatal(err) + } + + client = tp + resp, err := client.SendMessage(context.Background(), &gotfy.Message{ + Topic: "collinenlucy_nl", + Message: "Collin RSVP'd", + Title: "New RSVP", + Tags: []string{}, + Priority: gotfy.Default, + }) + + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%s\n", resp.ID) +} + +func SendRsvpNotification() { +} diff --git a/server/rsvp.go b/server/rsvp.go new file mode 100644 index 0000000..58a264d --- /dev/null +++ b/server/rsvp.go @@ -0,0 +1,129 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/url" + "strings" +) + +type Member struct { + name string + dietaryPreferences string + child bool +} + +func (m *Member) serialize() string { + builder := new(strings.Builder) + fmt.Fprintf(builder, "%s,%s,%t,", url.QueryEscape(m.name), url.QueryEscape(m.dietaryPreferences), m.child) + return builder.String() +} + +func deserializeMembers(serializedData string) ([]Member, error) { + members := make([]Member, 0) + keyVals := strings.Split(serializedData, ",") + for i := 0; i < len(keyVals)-1; i += 3 { + name, err := url.QueryUnescape(keyVals[i]) + if err != nil { + return nil, err + } + dietaryPreferences, err := url.QueryUnescape(keyVals[i+1]) + if err != nil { + return nil, err + } + child := keyVals[i+2] == "true" + + members = append(members, Member{ + name: name, + dietaryPreferences: dietaryPreferences, + child: child, + }) + } + return members, nil +} + +type Rsvp struct { + id int64 + attending bool + partySize int64 + partyMembers []Member +} + +func SetupRsvpsTable(db *sql.DB) { + _, err := db.Exec(`CREATE TABLE IF NOT EXISTS rsvps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + attending BOOLEAN NOT NULL, + partySize INTEGER NOT NULL, + partyMembers BLOB NOT NULL + );`) + + if err != nil { + log.Fatalf("failed to create rsvp table: %s", err.Error()) + } +} + +func GetRsvps(db *sql.DB) ([]Rsvp, error) { + rows, err := db.Query("SELECT * FROM rsvps ORDER BY id desc") + if err != nil { + return nil, err + } + + defer rows.Close() + + rsvps := make([]Rsvp, 0) + + for rows.Next() { + var id int64 + var attending bool + var partySize int64 + var rawPartyMembers string + + err = rows.Scan(&id, &attending, &partySize, &rawPartyMembers) + + if err != nil { + return nil, err + } + + members, err := deserializeMembers(rawPartyMembers) + + if err != nil { + return nil, err + } + + rsvps = append(rsvps, Rsvp{ + id: id, + attending: attending, + partySize: partySize, + partyMembers: members, + }) + } + + return rsvps, nil +} + +func (rsvp *Rsvp) CreateRsvp(db *sql.DB) (int64, error) { + partyMembersBuilder := new(strings.Builder) + for _, m := range rsvp.partyMembers { + partyMembersBuilder.WriteString(m.serialize()) + } + fmt.Printf("%s\n", partyMembersBuilder.String()) + result, err := db.Exec( + "INSERT INTO rsvps (attending, partySize, partyMembers) VALUES (?, ?, ?);", + rsvp.attending, + rsvp.partySize, + partyMembersBuilder.String(), + ) + + if err != nil { + return -1, err + } + + id, err := result.LastInsertId() + + if err != nil { + return -1, err + } + + return id, nil +}