From 1edc051ae21f8cc64d2d00796ad04dc8305d8dec Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 26 Jan 2025 00:48:19 +0000
Subject: [PATCH 01/60] fixed GetTOTP, started rough QR code implementation
GetTOTP handles TOTP method retrieval for confirmation and deletion.
QR code implementation looks like it's gonna suck, so might end up
using a library for this later.
---
admin/accounthttp.go | 11 +++--
admin/http.go | 16 +++++--
controller/qr.go | 107 +++++++++++++++++++++++++++++++++++++++++++
controller/totp.go | 9 ++--
main.go | 2 +-
5 files changed, 132 insertions(+), 13 deletions(-)
create mode 100644 controller/qr.go
diff --git a/admin/accounthttp.go b/admin/accounthttp.go
index 9402410..aa1e042 100644
--- a/admin/accounthttp.go
+++ b/admin/accounthttp.go
@@ -304,6 +304,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
return
}
+ fmt.Printf(
+ "TOTP:\n\tName: %s\n\tSecret: %s\n",
+ totp.Name,
+ totp.Secret,
+ )
+
confirmCode := controller.GenerateTOTP(totp.Secret, 0)
if code != confirmCode {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
@@ -330,12 +336,11 @@ func totpDeleteHandler(app *model.AppState) http.Handler {
return
}
- name := r.URL.Path
- fmt.Printf("%s\n", name);
- if len(name) == 0 {
+ if len(r.URL.Path) < 2 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
+ name := r.URL.Path[1:]
session := r.Context().Value("session").(*model.Session)
diff --git a/admin/http.go b/admin/http.go
index b6a71ee..42b7e46 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -19,6 +19,17 @@ import (
func Handler(app *model.AppState) http.Handler {
mux := http.NewServeMux()
+ mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ qrB64Img, err := controller.GenerateQRCode([]byte("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+
+ w.Write([]byte(" "))
+ }))
+
mux.Handle("/login", loginHandler(app))
mux.Handle("/logout", requireAccount(app, logoutHandler(app)))
@@ -243,11 +254,6 @@ func loginHandler(app *model.AppState) http.Handler {
return
}
- // new accounts won't have TOTP methods at first. there should be a
- // second phase of login that prompts the user for a TOTP *only*
- // if that account has a TOTP method.
- // TODO: login phases (username & password -> TOTP)
-
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
diff --git a/controller/qr.go b/controller/qr.go
new file mode 100644
index 0000000..c4c2520
--- /dev/null
+++ b/controller/qr.go
@@ -0,0 +1,107 @@
+package controller
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
+)
+
+const margin = 4
+
+type QRCodeECCLevel int64
+const (
+ LOW QRCodeECCLevel = iota
+ MEDIUM
+ QUARTILE
+ HIGH
+)
+
+func GenerateQRCode(data []byte) (string, error) {
+ version := 1
+
+ size := 0
+ size = 21 + version * 4
+ if version > 10 {
+ return "", errors.New(fmt.Sprintf("QR version %d not supported", version))
+ }
+
+ img := image.NewGray(image.Rect(0, 0, size + margin * 2, size + margin * 2))
+
+ // fill white
+ for y := range size + margin * 2 {
+ for x := range size + margin * 2 {
+ img.Set(x, y, color.White)
+ }
+ }
+
+ // draw alignment squares
+ drawLargeAlignmentSquare(margin, margin, img)
+ drawLargeAlignmentSquare(margin, margin + size - 7, img)
+ drawLargeAlignmentSquare(margin + size - 7, margin, img)
+ drawSmallAlignmentSquare(size - 5, size - 5, img)
+ /*
+ if version > 4 {
+ space := version * 3 - 2
+ end := size / space
+ for y := range size / space + 1 {
+ for x := range size / space + 1 {
+ if x == 0 && y == 0 { continue }
+ if x == 0 && y == end { continue }
+ if x == end && y == 0 { continue }
+ if x == end && y == end { continue }
+ drawSmallAlignmentSquare(
+ x * space + margin + 4,
+ y * space + margin + 4,
+ img,
+ )
+ }
+ }
+ }
+ */
+
+ // draw timing bits
+ for i := margin + 6; i < size - 4; i++ {
+ if (i % 2 == 0) {
+ img.Set(i, margin + 6, color.Black)
+ img.Set(margin + 6, i, color.Black)
+ }
+ }
+ img.Set(margin + 8, size - 4, color.Black)
+
+ var imgBuf bytes.Buffer
+ err := png.Encode(&imgBuf, img)
+ if err != nil {
+ return "", err
+ }
+
+ base64Img := base64.StdEncoding.EncodeToString(imgBuf.Bytes())
+
+ return "data:image/png;base64," + base64Img, nil
+}
+
+func drawLargeAlignmentSquare(x int, y int, img *image.Gray) {
+ for yi := range 7 {
+ for xi := range 7 {
+ if (xi == 0 || xi == 6) || (yi == 0 || yi == 6) {
+ img.Set(x + xi, y + yi, color.Black)
+ } else if (xi > 1 && xi < 5) && (yi > 1 && yi < 5) {
+ img.Set(x + xi, y + yi, color.Black)
+ }
+ }
+ }
+}
+
+func drawSmallAlignmentSquare(x int, y int, img *image.Gray) {
+ for yi := range 5 {
+ for xi := range 5 {
+ if (xi == 0 || xi == 4) || (yi == 0 || yi == 4) {
+ img.Set(x + xi, y + yi, color.Black)
+ }
+ }
+ }
+ img.Set(x + 2, y + 2, color.Black)
+}
diff --git a/controller/totp.go b/controller/totp.go
index bc71747..dbbeec7 100644
--- a/controller/totp.go
+++ b/controller/totp.go
@@ -64,9 +64,9 @@ func GenerateTOTPURI(username string, secret string) string {
query := url.Query()
query.Set("secret", secret)
query.Set("issuer", "arimelody.me")
- query.Set("algorithm", "SHA1")
- query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH))
- query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP))
+ // query.Set("algorithm", "SHA1")
+ // query.Set("digits", fmt.Sprintf("%d", TOTP_CODE_LENGTH))
+ // query.Set("period", fmt.Sprintf("%d", TOTP_TIME_STEP))
url.RawQuery = query.Encode()
return url.String()
@@ -116,8 +116,9 @@ func GetTOTP(db *sqlx.DB, accountID string, name string) (*model.TOTP, error) {
err := db.Get(
&totp,
"SELECT * FROM totp " +
- "WHERE account=$1",
+ "WHERE account=$1 AND name=$2",
accountID,
+ name,
)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
diff --git a/main.go b/main.go
index 251d9a9..ac1de59 100644
--- a/main.go
+++ b/main.go
@@ -89,7 +89,6 @@ func main() {
}
username := os.Args[2]
totpName := os.Args[3]
- secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
account, err := controller.GetAccountByUsername(app.DB, username)
if err != nil {
@@ -102,6 +101,7 @@ func main() {
os.Exit(1)
}
+ secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
totp := model.TOTP {
AccountID: account.ID,
Name: totpName,
From 3450d879acd0662f8bfae1d010f6e02ee9441e06 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 26 Jan 2025 20:09:18 +0000
Subject: [PATCH 02/60] QR codes complete, account settings finished!
+ refactored templates a little; this might need more work!
---
admin/accounthttp.go | 31 ++++---
admin/artisthttp.go | 2 +-
admin/http.go | 12 +--
admin/releasehttp.go | 16 ++--
admin/templates.go | 150 ++++++++++++++++++----------------
admin/trackhttp.go | 2 +-
admin/views/totp-confirm.html | 17 ++--
controller/qr.go | 15 +++-
go.mod | 6 +-
go.sum | 6 +-
main.go | 2 +-
templates/templates.go | 47 +++++------
view/music.go | 4 +-
13 files changed, 175 insertions(+), 135 deletions(-)
diff --git a/admin/accounthttp.go b/admin/accounthttp.go
index aa1e042..408b4c5 100644
--- a/admin/accounthttp.go
+++ b/admin/accounthttp.go
@@ -63,7 +63,7 @@ func accountIndexHandler(app *model.AppState) http.Handler {
session.Message = sessionMessage
session.Error = sessionError
- err = pages["account"].Execute(w, accountResponse{
+ err = accountTemplate.Execute(w, accountResponse{
Session: session,
TOTPs: totps,
})
@@ -199,7 +199,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err := pages["totp-setup"].Execute(w, totpSetupData{ Session: session })
+ err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -216,6 +216,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
Session *model.Session
TOTP *model.TOTP
NameEscaped string
+ QRBase64Image string
}
err := r.ParseForm()
@@ -242,7 +243,7 @@ func totpSetupHandler(app *model.AppState) http.Handler {
if err != nil {
fmt.Printf("WARN: Failed to create TOTP method: %s\n", err)
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
- err := pages["totp-setup"].Execute(w, totpSetupData{ Session: session })
+ err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
if err != nil {
fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -250,10 +251,24 @@ func totpSetupHandler(app *model.AppState) http.Handler {
return
}
- err = pages["totp-confirm"].Execute(w, totpSetupData{
+ qrBase64Image, err := controller.GenerateQRCode(
+ controller.GenerateTOTPURI(session.Account.Username, totp.Secret))
+ if err != nil {
+ fmt.Printf("WARN: Failed to generate TOTP setup QR code: %s\n", err)
+ controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
+ err := totpSetupTemplate.Execute(w, totpSetupData{ Session: session })
+ if err != nil {
+ fmt.Printf("WARN: Failed to render TOTP setup page: %s\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ return
+ }
+
+ err = totpConfirmTemplate.Execute(w, totpSetupData{
Session: session,
TOTP: &totp,
NameEscaped: url.PathEscape(totp.Name),
+ QRBase64Image: qrBase64Image,
})
if err != nil {
fmt.Printf("WARN: Failed to render TOTP confirm page: %s\n", err)
@@ -304,18 +319,12 @@ func totpConfirmHandler(app *model.AppState) http.Handler {
return
}
- fmt.Printf(
- "TOTP:\n\tName: %s\n\tSecret: %s\n",
- totp.Name,
- totp.Secret,
- )
-
confirmCode := controller.GenerateTOTP(totp.Secret, 0)
if code != confirmCode {
confirmCodeOffset := controller.GenerateTOTP(totp.Secret, 1)
if code != confirmCodeOffset {
controller.SetSessionError(app.DB, session, "Incorrect TOTP code. Please try again.")
- err = pages["totp-confirm"].Execute(w, totpConfirmData{
+ err = totpConfirmTemplate.Execute(w, totpConfirmData{
Session: session,
TOTP: totp,
})
diff --git a/admin/artisthttp.go b/admin/artisthttp.go
index 5979493..6dfbbfd 100644
--- a/admin/artisthttp.go
+++ b/admin/artisthttp.go
@@ -39,7 +39,7 @@ func serveArtist(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err = pages["artist"].Execute(w, ArtistResponse{
+ err = artistTemplate.Execute(w, ArtistResponse{
Session: session,
Artist: artist,
Credits: credits,
diff --git a/admin/http.go b/admin/http.go
index 42b7e46..6fd8f59 100644
--- a/admin/http.go
+++ b/admin/http.go
@@ -20,7 +20,7 @@ func Handler(app *model.AppState) http.Handler {
mux := http.NewServeMux()
mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- qrB64Img, err := controller.GenerateQRCode([]byte("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family"))
+ qrB64Img, err := controller.GenerateQRCode("super epic mega gaming test message. be sure to buy free2play on bandcamp so i can put food on my family")
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to generate QR code: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -87,7 +87,7 @@ func AdminIndexHandler(app *model.AppState) http.Handler {
Tracks []*model.Track
}
- err = pages["index"].Execute(w, IndexData{
+ err = indexTemplate.Execute(w, IndexData{
Session: session,
Releases: releases,
Artists: artists,
@@ -116,7 +116,7 @@ func registerAccountHandler(app *model.AppState) http.Handler {
}
render := func() {
- err := pages["register"].Execute(w, registerData{ Session: session })
+ err := registerTemplate.Execute(w, registerData{ Session: session })
if err != nil {
fmt.Printf("WARN: Error rendering create account page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -229,7 +229,7 @@ func loginHandler(app *model.AppState) http.Handler {
}
render := func() {
- err := pages["login"].Execute(w, loginData{ Session: session })
+ err := loginTemplate.Execute(w, loginData{ Session: session })
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -307,7 +307,7 @@ func loginHandler(app *model.AppState) http.Handler {
Username string
Password string
}
- err = pages["login-totp"].Execute(w, loginTOTPData{
+ err = loginTOTPTemplate.Execute(w, loginTOTPData{
Session: session,
Username: credentials.Username,
Password: credentials.Password,
@@ -379,7 +379,7 @@ func logoutHandler(app *model.AppState) http.Handler {
Path: "/",
})
- err = pages["logout"].Execute(w, nil)
+ err = logoutTemplate.Execute(w, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to render logout page: %v\n", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/admin/releasehttp.go b/admin/releasehttp.go
index cd73e98..be5052b 100644
--- a/admin/releasehttp.go
+++ b/admin/releasehttp.go
@@ -60,7 +60,7 @@ func serveRelease(app *model.AppState) http.Handler {
Release *model.Release
}
- err = pages["release"].Execute(w, ReleaseResponse{
+ err = releaseTemplate.Execute(w, ReleaseResponse{
Session: session,
Release: release,
})
@@ -74,7 +74,7 @@ func serveRelease(app *model.AppState) http.Handler {
func serveEditCredits(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
- err := components["editcredits"].Execute(w, release)
+ err := editCreditsTemplate.Execute(w, release)
if err != nil {
fmt.Printf("Error rendering edit credits component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -97,7 +97,7 @@ func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = components["addcredit"].Execute(w, response{
+ err = addCreditTemplate.Execute(w, response{
ReleaseID: release.ID,
Artists: artists,
})
@@ -123,7 +123,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = components["newcredit"].Execute(w, artist)
+ err = newCreditTemplate.Execute(w, artist)
if err != nil {
fmt.Printf("Error rendering new credit component for %s: %s\n", artist.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -134,7 +134,7 @@ func serveNewCredit(app *model.AppState) http.Handler {
func serveEditLinks(release *model.Release) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
- err := components["editlinks"].Execute(w, release)
+ err := editLinksTemplate.Execute(w, release)
if err != nil {
fmt.Printf("Error rendering edit links component for %s: %s\n", release.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@@ -151,7 +151,7 @@ func serveEditTracks(release *model.Release) http.Handler {
Add func(a int, b int) int
}
- err := components["edittracks"].Execute(w, editTracksData{
+ err := editTracksTemplate.Execute(w, editTracksData{
Release: release,
Add: func(a, b int) int { return a + b },
})
@@ -177,7 +177,7 @@ func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = components["addtrack"].Execute(w, response{
+ err = addTrackTemplate.Execute(w, response{
ReleaseID: release.ID,
Tracks: tracks,
})
@@ -204,7 +204,7 @@ func serveNewTrack(app *model.AppState) http.Handler {
}
w.Header().Set("Content-Type", "text/html")
- err = components["newtrack"].Execute(w, track)
+ err = newTrackTemplate.Execute(w, track)
if err != nil {
fmt.Printf("Error rendering new track component for %s: %s\n", track.ID, err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
diff --git a/admin/templates.go b/admin/templates.go
index d9a74ca..49c118b 100644
--- a/admin/templates.go
+++ b/admin/templates.go
@@ -1,80 +1,90 @@
package admin
import (
- "html/template"
- "path/filepath"
+ "html/template"
+ "path/filepath"
)
-var pages = map[string]*template.Template{
- "index": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "components", "release", "release-list-item.html"),
- filepath.Join("admin", "views", "index.html"),
- )),
+var indexTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "components", "release", "release-list-item.html"),
+ filepath.Join("admin", "views", "index.html"),
+))
- "login": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "login.html"),
- )),
- "login-totp": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "login-totp.html"),
- )),
- "register": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "register.html"),
- )),
- "logout": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "logout.html"),
- )),
- "account": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-account.html"),
- )),
- "totp-setup": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "totp-setup.html"),
- )),
- "totp-confirm": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "totp-confirm.html"),
- )),
+var loginTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "login.html"),
+))
+var loginTOTPTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "login-totp.html"),
+))
+var registerTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "register.html"),
+))
+var logoutTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "logout.html"),
+))
+var accountTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "edit-account.html"),
+))
+var totpSetupTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "totp-setup.html"),
+))
+var totpConfirmTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "totp-confirm.html"),
+))
- "release": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-release.html"),
- )),
- "artist": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "views", "edit-artist.html"),
- )),
- "track": template.Must(template.ParseFiles(
- filepath.Join("admin", "views", "layout.html"),
- filepath.Join("views", "prideflag.html"),
- filepath.Join("admin", "components", "release", "release-list-item.html"),
- filepath.Join("admin", "views", "edit-track.html"),
- )),
-}
+var releaseTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "edit-release.html"),
+))
+var artistTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "views", "edit-artist.html"),
+))
+var trackTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "views", "layout.html"),
+ filepath.Join("views", "prideflag.html"),
+ filepath.Join("admin", "components", "release", "release-list-item.html"),
+ filepath.Join("admin", "views", "edit-track.html"),
+))
-var components = map[string]*template.Template{
- "editcredits": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "editcredits.html"))),
- "addcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "addcredit.html"))),
- "newcredit": template.Must(template.ParseFiles(filepath.Join("admin", "components", "credits", "newcredit.html"))),
+var editCreditsTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "credits", "editcredits.html"),
+))
+var addCreditTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "credits", "addcredit.html"),
+))
+var newCreditTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "credits", "newcredit.html"),
+))
- "editlinks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "links", "editlinks.html"))),
+var editLinksTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "links", "editlinks.html"),
+))
- "edittracks": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "edittracks.html"))),
- "addtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "addtrack.html"))),
- "newtrack": template.Must(template.ParseFiles(filepath.Join("admin", "components", "tracks", "newtrack.html"))),
-}
+var editTracksTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "tracks", "edittracks.html"),
+))
+var addTrackTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "tracks", "addtrack.html"),
+))
+var newTrackTemplate = template.Must(template.ParseFiles(
+ filepath.Join("admin", "components", "tracks", "newtrack.html"),
+))
diff --git a/admin/trackhttp.go b/admin/trackhttp.go
index 9436671..a92f81a 100644
--- a/admin/trackhttp.go
+++ b/admin/trackhttp.go
@@ -39,7 +39,7 @@ func serveTrack(app *model.AppState) http.Handler {
session := r.Context().Value("session").(*model.Session)
- err = pages["track"].Execute(w, TrackResponse{
+ err = trackTemplate.Execute(w, TrackResponse{
Session: session,
Track: track,
Releases: releases,
diff --git a/admin/views/totp-confirm.html b/admin/views/totp-confirm.html
index ac39e52..b0810e2 100644
--- a/admin/views/totp-confirm.html
+++ b/admin/views/totp-confirm.html
@@ -3,6 +3,9 @@
\ No newline at end of file
diff --git a/public/img/brand/twitch.svg b/public/img/brand/twitch.svg
new file mode 100644
index 0000000..3120fea
--- /dev/null
+++ b/public/img/brand/twitch.svg
@@ -0,0 +1,21 @@
+
+
+
+
+Asset 2
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/img/brand/youtube.svg b/public/img/brand/youtube.svg
new file mode 100644
index 0000000..3286071
--- /dev/null
+++ b/public/img/brand/youtube.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/public/script/index.js b/public/script/index.js
index 512ed8f..2197bd4 100644
--- a/public/script/index.js
+++ b/public/script/index.js
@@ -1,3 +1,5 @@
+import { hijackClickEvent } from "./main.js";
+
const hexPrimary = document.getElementById("hex-primary");
const hexSecondary = document.getElementById("hex-secondary");
const hexTertiary = document.getElementById("hex-tertiary");
@@ -14,3 +16,8 @@ updateHexColours();
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
updateHexColours();
});
+
+document.querySelectorAll("ul#projects li.project-item").forEach(projectItem => {
+ const link = projectItem.querySelector('a');
+ hijackClickEvent(projectItem, link);
+});
diff --git a/public/script/main.js b/public/script/main.js
index c1d5101..19eddc2 100644
--- a/public/script/main.js
+++ b/public/script/main.js
@@ -45,6 +45,23 @@ function fill_list(list) {
});
}
+export function hijackClickEvent(container, link) {
+ container.addEventListener('click', event => {
+ if (event.target.tagName.toLowerCase() === 'a') return;
+ event.preventDefault();
+ link.dispatchEvent(new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ ctrlKey: event.ctrlKey,
+ metaKey: event.metaKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ button: event.button,
+ }));
+ });
+}
+
document.addEventListener("DOMContentLoaded", () => {
[...document.querySelectorAll(".typeout")]
.filter((e) => e.innerText != "")
diff --git a/public/script/music.js b/public/script/music.js
index 273ce2b..91f34ab 100644
--- a/public/script/music.js
+++ b/public/script/music.js
@@ -1,12 +1,6 @@
-import "./main.js";
+import { hijackClickEvent } from "./main.js";
document.querySelectorAll("div.music").forEach(container => {
- const link = container.querySelector(".music-title a").href
-
- container.addEventListener("click", event => {
- if (event.target.href) return;
-
- event.preventDefault();
- location = link;
- });
+ const link = container.querySelector(".music-title a")
+ hijackClickEvent(container, link);
});
diff --git a/public/style/index.css b/public/style/index.css
index f3cb761..6edb362 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -96,30 +96,36 @@ hr {
overflow: visible;
}
-ul.links {
+ul.platform-links {
+ padding-left: 1em;
display: flex;
- gap: 1em .5em;
+ gap: .5em;
flex-wrap: wrap;
}
-ul.links li {
+ul.platform-links li {
list-style: none;
}
-ul.links li a {
+ul.platform-links li a {
padding: .4em .5em;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: .5em;
border: 1px solid var(--links);
color: var(--links);
border-radius: 2px;
background-color: transparent;
- transition-property: color, border-color, background-color;
+ transition-property: color, border-color, background-color, box-shadow;
transition-duration: .2s;
animation-delay: 0s;
animation: list-item-fadein .2s forwards;
opacity: 0;
}
-ul.links li a:hover {
+ul.platform-links li a:hover {
color: #eee;
border-color: #eee;
background-color: var(--links) !important;
@@ -127,6 +133,75 @@ ul.links li a:hover {
box-shadow: 0 0 1em var(--links);
}
+ul.platform-links li a img {
+ height: 1em;
+ width: 1em;
+}
+
+ul#projects {
+ padding: 0;
+ list-style: none;
+}
+
+li.project-item {
+ padding: .5em;
+ border: 1px solid var(--links);
+ margin: 1em 0;
+ display: flex;
+ flex-direction: row;
+ gap: .5em;
+ border-radius: 2px;
+ transition-property: color, border-color, background-color, box-shadow;
+ transition-duration: .2s;
+ cursor: pointer;
+}
+li.project-item a {
+ transition: color .2s linear;
+}
+
+li.project-item:hover {
+ color: #eee;
+ border-color: #eee;
+ background-color: var(--links) !important;
+ text-decoration: none;
+ box-shadow: 0 0 1em var(--links);
+}
+li.project-item:hover a {
+ color: #eee;
+}
+
+li.project-item .project-info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+li.project-item img.project-icon {
+ width: 2.5em;
+ height: 2.5em;
+ object-fit: cover;
+ border-radius: 2px;
+}
+
+li.project-item span.project-icon {
+ font-size: 2em;
+ display: block;
+ width: 45px;
+ height: 45px;
+ text-align: center;
+ /* background: #0004; */
+ /* border: 1px solid var(--on-background); */
+ border-radius: 2px;
+}
+
+li.project-item a {
+ text-decoration: none;
+}
+
+li.project-item p {
+ margin: 0;
+}
+
div#web-buttons {
margin: 2rem 0;
}
@@ -147,4 +222,3 @@ div#web-buttons {
transform: translate(-2px, -2px);
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
}
-
diff --git a/views/index.html b/views/index.html
index 070d648..f639c55 100644
--- a/views/index.html
+++ b/views/index.html
@@ -17,7 +17,7 @@
-
+
{{end}}
{{define "content"}}
@@ -33,7 +33,7 @@
- i'm a musician , developer ,
+ i'm a musician , developer ,
streamer , youtuber ,
and probably a bunch of other things i forgot to mention!
@@ -44,7 +44,7 @@
if you're looking to support me financially, that's so cool of you!!
if you like, you can buy some of my music over on
- bandcamp
+ bandcamp
so you can at least get something for your money.
thank you very much either way!! 💕
@@ -84,65 +84,119 @@
where to find me 🛰️
+
+
projects i've worked on 🛠️
-
-
-
- mcstatusface
-
+
@@ -156,40 +210,40 @@
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -205,16 +259,16 @@
-
+
-
+
-
+
From f7b3faf8e89b240ca1e65eb698734039fcae9ce3 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Mon, 16 Jun 2025 22:41:54 +0100
Subject: [PATCH 45/60] move source link from header to footer
---
public/style/header.css | 5 ++---
views/footer.html | 7 ++++++-
views/header.html | 3 ---
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/public/style/header.css b/public/style/header.css
index 531649a..f399d4d 100644
--- a/public/style/header.css
+++ b/public/style/header.css
@@ -25,7 +25,6 @@ nav {
flex-grow: 1;
display: flex;
gap: .5em;
- cursor: pointer;
}
img#header-icon {
@@ -36,19 +35,19 @@ img#header-icon {
}
#header-text {
- width: 11em;
display: flex;
flex-direction: column;
justify-content: center;
- flex-grow: 1;
}
#header-text h1 {
+ width: fit-content;
margin: 0;
font-size: 1em;
}
#header-text h2 {
+ width: fit-content;
height: 1.2em;
line-height: 1.2em;
margin: 0;
diff --git a/views/footer.html b/views/footer.html
index 217c4b5..eccf125 100644
--- a/views/footer.html
+++ b/views/footer.html
@@ -2,7 +2,12 @@
diff --git a/views/header.html b/views/header.html
index 291b8ac..03a8384 100644
--- a/views/header.html
+++ b/views/header.html
@@ -23,9 +23,6 @@
music
-
- source
-
blog
From 9274796729a56290d788106e0ed5327643103e4a Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 17 Jun 2025 01:15:08 +0100
Subject: [PATCH 46/60] early implementation of ari melody LIVE tracker
---
controller/config.go | 4 ++
controller/twitch.go | 97 ++++++++++++++++++++++++++++++++++++++++
main.go | 47 ++++---------------
model/appstate.go | 8 ++++
model/twitch.go | 43 ++++++++++++++++++
public/style/colours.css | 1 +
public/style/index.css | 81 +++++++++++++++++++++++++++++++++
view/index.go | 45 +++++++++++++++++++
view/static.go | 31 +++++++++++++
views/index.html | 17 +++++++
10 files changed, 335 insertions(+), 39 deletions(-)
create mode 100644 controller/twitch.go
create mode 100644 model/twitch.go
create mode 100644 view/index.go
create mode 100644 view/static.go
diff --git a/controller/config.go b/controller/config.go
index 1d3cbbb..fdfa756 100644
--- a/controller/config.go
+++ b/controller/config.go
@@ -77,5 +77,9 @@ func handleConfigOverrides(config *model.Config) error {
if env, has := os.LookupEnv("ARIMELODY_DISCORD_CLIENT_ID"); has { config.Discord.ClientID = env }
if env, has := os.LookupEnv("ARIMELODY_DISCORD_SECRET"); has { config.Discord.Secret = env }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_BROADCASTER"); has { config.Twitch.Broadcaster = env }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_CLIENT_ID"); has { config.Twitch.ClientID = env }
+ if env, has := os.LookupEnv("ARIMELODY_TWITCH_SECRET"); has { config.Twitch.Secret = env }
+
return nil
}
diff --git a/controller/twitch.go b/controller/twitch.go
new file mode 100644
index 0000000..f7dc509
--- /dev/null
+++ b/controller/twitch.go
@@ -0,0 +1,97 @@
+package controller
+
+import (
+ "arimelody-web/model"
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
+
+func TwitchSetup(app *model.AppState) error {
+ app.Twitch = &model.TwitchState{}
+ err := RefreshTwitchToken(app)
+ return err
+}
+
+func RefreshTwitchToken(app *model.AppState) error {
+ if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
+ return nil
+ }
+
+ requestUrl, _ := url.Parse("https://id.twitch.tv/oauth2/token")
+ req, _ := http.NewRequest(http.MethodPost, requestUrl.String(), bytes.NewBuffer([]byte(url.Values{
+ "client_id": []string{ app.Config.Twitch.ClientID },
+ "client_secret": []string{ app.Config.Twitch.Secret },
+ "grant_type": []string{ "client_credentials" },
+ }.Encode())))
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+
+ type TwitchOAuthToken struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ }
+ oauthResponse := TwitchOAuthToken{}
+ err = json.NewDecoder(res.Body).Decode(&oauthResponse)
+ if err != nil {
+ return err
+ }
+
+ app.Twitch.Token = &model.TwitchOAuthToken{
+ AccessToken: oauthResponse.AccessToken,
+ ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
+ TokenType: oauthResponse.TokenType,
+ }
+
+ return nil
+}
+
+var lastStreamState *model.TwitchStreamInfo
+var lastStreamStateAt time.Time
+
+func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStreamInfo, error) {
+ if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
+ return lastStreamState, nil
+ }
+
+ fmt.Print("MAKING COSTLY REQUEST TO TWITCH.TV API...\n")
+
+ requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
+ requestUrl.RawQuery = url.Values{
+ "user_login": []string{ broadcaster },
+ }.Encode()
+ req, _ := http.NewRequest(http.MethodGet, requestUrl.String(), nil)
+ req.Header.Set("Client-Id", app.Config.Twitch.ClientID)
+ req.Header.Set("Authorization", "Bearer " + app.Twitch.Token.AccessToken)
+
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ type StreamsResponse struct {
+ Data []model.TwitchStreamInfo `json:"data"`
+ }
+ streamInfo := StreamsResponse{}
+ err = json.NewDecoder(res.Body).Decode(&streamInfo)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(streamInfo.Data) == 0 {
+ return nil, nil
+ }
+
+ lastStreamState = &streamInfo.Data[0]
+ lastStreamStateAt = time.Now().UTC()
+ return lastStreamState, nil
+}
diff --git a/main.go b/main.go
index 29539ac..8f37639 100644
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
"arimelody-web/cursor"
"arimelody-web/log"
"arimelody-web/model"
- "arimelody-web/templates"
"arimelody-web/view"
"github.com/jmoiron/sqlx"
@@ -40,6 +39,7 @@ func main() {
app := model.AppState{
Config: controller.GetConfig(),
+ Twitch: nil,
}
// initialise database connection
@@ -460,6 +460,11 @@ func main() {
// handle DB migrations
controller.CheckDBVersionAndMigrate(app.DB)
+ err = controller.TwitchSetup(&app)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ }
+
// initial invite code
accountsCount := 0
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
@@ -511,49 +516,13 @@ func createServeMux(app *model.AppState) *http.ServeMux {
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
- mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
+ mux.Handle("/uploads/", http.StripPrefix("/uploads", view.StaticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
mux.Handle("/cursor-ws", cursor.Handler(app))
- mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodHead {
- w.WriteHeader(http.StatusOK)
- return
- }
-
- if r.URL.Path == "/" || r.URL.Path == "/index.html" {
- err := templates.IndexTemplate.Execute(w, nil)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- }
- return
- }
- staticHandler("public").ServeHTTP(w, r)
- }))
+ mux.Handle("/", view.IndexHandler(app))
return mux
}
-func staticHandler(directory string) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
-
- // does the file exist?
- if err != nil {
- if errors.Is(err, os.ErrNotExist) {
- http.NotFound(w, r)
- return
- }
- }
-
- // is thjs a directory? (forbidden)
- if info.IsDir() {
- http.NotFound(w, r)
- return
- }
-
- http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
- })
-}
-
var PoweredByStrings = []string{
"nerd rage",
"estrogen",
diff --git a/model/appstate.go b/model/appstate.go
index a910a29..861a991 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -21,6 +21,12 @@ type (
Secret string `toml:"secret"`
}
+ TwitchConfig struct {
+ Broadcaster string `toml:"broadcaster"`
+ ClientID string `toml:"client_id"`
+ Secret string `toml:"secret"`
+ }
+
Config struct {
BaseUrl string `toml:"base_url" comment:"Used for OAuth redirects."`
Host string `toml:"host"`
@@ -29,11 +35,13 @@ type (
TrustedProxies []string `toml:"trusted_proxies"`
DB DBConfig `toml:"db"`
Discord DiscordConfig `toml:"discord"`
+ Twitch TwitchConfig `toml:"twitch"`
}
AppState struct {
DB *sqlx.DB
Config Config
Log log.Logger
+ Twitch *TwitchState
}
)
diff --git a/model/twitch.go b/model/twitch.go
new file mode 100644
index 0000000..6bca17d
--- /dev/null
+++ b/model/twitch.go
@@ -0,0 +1,43 @@
+package model
+
+import (
+ "fmt"
+ "strings"
+ "time"
+)
+
+type (
+ TwitchOAuthToken struct {
+ AccessToken string
+ ExpiresAt time.Time
+ TokenType string
+ }
+
+ TwitchState struct {
+ Token *TwitchOAuthToken
+ }
+
+ TwitchStreamInfo struct {
+ ID string `json:"id"`
+ UserID string `json:"user_id"`
+ UserLogin string `json:"user_login"`
+ UserName string `json:"user_name"`
+ GameID string `json:"game_id"`
+ GameName string `json:"game_name"`
+ Type string `json:"type"`
+ Title string `json:"title"`
+ ViewerCount int `json:"viewer_count"`
+ StartedAt string `json:"started_at"`
+ Language string `json:"language"`
+ ThumbnailURL string `json:"thumbnail_url"`
+ TagIDs []string `json:"tag_ids"`
+ Tags []string `json:"tags"`
+ IsMature bool `json:"is_mature"`
+ }
+)
+
+func (info *TwitchStreamInfo) Thumbnail(width int, height int) string {
+ res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
+ res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
+ return res
+}
diff --git a/public/style/colours.css b/public/style/colours.css
index 2bf607d..de63198 100644
--- a/public/style/colours.css
+++ b/public/style/colours.css
@@ -6,6 +6,7 @@
--secondary: #f8e05b;
--tertiary: #f788fe;
--links: #5eb2ff;
+ --live: #fd3737;
}
@media (prefers-color-scheme: light) {
diff --git a/public/style/index.css b/public/style/index.css
index 6edb362..f60fdb7 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -222,3 +222,84 @@ div#web-buttons {
transform: translate(-2px, -2px);
box-shadow: 1px 1px 0 #eee, 2px 2px 0 #eee;
}
+
+#live-banner {
+ margin: 1em 0 2em 0;
+ padding: 1em;
+ border-radius: 4px;
+ border: 1px solid var(--primary);
+ box-shadow: 0 0 8px var(--primary);
+}
+
+#live-banner h2 {
+ margin: 0 0 .4em 0;
+ color: var(--on-background);
+}
+
+#live-banner p {
+ margin: 0;
+}
+
+.live-highlight {
+ color: var(--primary);
+}
+
+.live-preview {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ gap: 1em;
+}
+
+.live-preview div:first-of-type {
+ text-align: center;
+}
+
+.live-thumbnail {
+ border-radius: 4px;
+}
+
+.live-button {
+ margin: .2em;
+ padding: .4em .5em;
+ display: inline-block;
+ color: var(--primary);
+ border: 1px solid var(--primary);
+ border-radius: 4px;
+ transition: color .1s linear, background-color .1s linear, box-shadow .1s linear;
+}
+
+.live-button:hover {
+ color: var(--background);
+ background-color: var(--primary);
+ box-shadow: 0 0 8px var(--primary);
+ text-decoration: none;
+}
+
+.live-info {
+ display: flex;
+ flex-direction: column;
+ gap: .3em;
+ overflow-x: hidden;
+}
+
+.live-game {
+ overflow: hidden;
+ text-wrap: nowrap;
+ text-overflow: ellipsis;
+}
+
+.live-game .live-game-prefix {
+ opacity: .8;
+}
+
+.live-title {
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.live-viewers {
+ opacity: .5;
+}
diff --git a/view/index.go b/view/index.go
new file mode 100644
index 0000000..995de44
--- /dev/null
+++ b/view/index.go
@@ -0,0 +1,45 @@
+package view
+
+import (
+ "arimelody-web/controller"
+ "arimelody-web/model"
+ "arimelody-web/templates"
+ "fmt"
+ "net/http"
+ "os"
+)
+
+func IndexHandler(app *model.AppState) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodHead {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ type IndexData struct {
+ TwitchStatus *model.TwitchStreamInfo
+ }
+
+ var err error
+ var twitchStatus *model.TwitchStreamInfo = nil
+ if len(app.Config.Twitch.Broadcaster) > 0 {
+ twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
+ }
+ }
+
+ if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+ err := templates.IndexTemplate.Execute(w, IndexData{
+ TwitchStatus: twitchStatus,
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to render index page: %v\n", err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ return
+ }
+
+ StaticHandler("public").ServeHTTP(w, r)
+ })
+}
diff --git a/view/static.go b/view/static.go
new file mode 100644
index 0000000..52263a2
--- /dev/null
+++ b/view/static.go
@@ -0,0 +1,31 @@
+package view
+
+import (
+ "errors"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+func StaticHandler(directory string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
+
+ // does the file exist?
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ http.NotFound(w, r)
+ return
+ }
+ }
+
+ // is thjs a directory? (forbidden)
+ if info.IsDir() {
+ http.NotFound(w, r)
+ return
+ }
+
+ http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
+ })
+}
+
diff --git a/views/index.html b/views/index.html
index f639c55..98a862c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -22,6 +22,23 @@
{{define "content"}}
+ {{if .TwitchStatus}}
+
+
ari is LIVE right now!
+
+
+
+
streaming: {{.TwitchStatus.GameName}}
+
{{.TwitchStatus.Title}}
+
{{.TwitchStatus.ViewerCount}} viewers
+
+
+
+ {{end}}
+
# hello, world!
From 581273370de2029c8258564ab2a180eea93f0a0d Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 17 Jun 2025 02:01:06 +0100
Subject: [PATCH 47/60] improvements to LIVE tracker
---
controller/twitch.go | 3 ---
main.go | 8 +++++---
model/appstate.go | 4 ++--
public/style/index.css | 41 +++++++++++++++++++++++++++++++++++------
public/style/main.css | 1 +
view/index.go | 2 +-
views/index.html | 2 +-
7 files changed, 45 insertions(+), 16 deletions(-)
diff --git a/controller/twitch.go b/controller/twitch.go
index f7dc509..98d3b9a 100644
--- a/controller/twitch.go
+++ b/controller/twitch.go
@@ -4,7 +4,6 @@ import (
"arimelody-web/model"
"bytes"
"encoding/json"
- "fmt"
"net/http"
"net/url"
"time"
@@ -63,8 +62,6 @@ func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStre
return lastStreamState, nil
}
- fmt.Print("MAKING COSTLY REQUEST TO TWITCH.TV API...\n")
-
requestUrl, _ := url.Parse(TWITCH_API_BASE + "streams")
requestUrl.RawQuery = url.Values{
"user_login": []string{ broadcaster },
diff --git a/main.go b/main.go
index 8f37639..0234d79 100644
--- a/main.go
+++ b/main.go
@@ -460,9 +460,11 @@ func main() {
// handle DB migrations
controller.CheckDBVersionAndMigrate(app.DB)
- err = controller.TwitchSetup(&app)
- if err != nil {
- fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ if app.Config.Twitch != nil {
+ err = controller.TwitchSetup(&app)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
+ }
}
// initial invite code
diff --git a/model/appstate.go b/model/appstate.go
index 861a991..3a1c230 100644
--- a/model/appstate.go
+++ b/model/appstate.go
@@ -34,8 +34,8 @@ type (
DataDirectory string `toml:"data_dir"`
TrustedProxies []string `toml:"trusted_proxies"`
DB DBConfig `toml:"db"`
- Discord DiscordConfig `toml:"discord"`
- Twitch TwitchConfig `toml:"twitch"`
+ Discord *DiscordConfig `toml:"discord"`
+ Twitch *TwitchConfig `toml:"twitch"`
}
AppState struct {
diff --git a/public/style/index.css b/public/style/index.css
index f60fdb7..6efe241 100644
--- a/public/style/index.css
+++ b/public/style/index.css
@@ -231,11 +231,6 @@ div#web-buttons {
box-shadow: 0 0 8px var(--primary);
}
-#live-banner h2 {
- margin: 0 0 .4em 0;
- color: var(--on-background);
-}
-
#live-banner p {
margin: 0;
}
@@ -252,7 +247,11 @@ div#web-buttons {
}
.live-preview div:first-of-type {
- text-align: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ gap: .3em;
}
.live-thumbnail {
@@ -283,6 +282,36 @@ div#web-buttons {
overflow-x: hidden;
}
+#live-banner h2 {
+ margin: 0;
+ color: var(--on-background);
+ font-family: 'Inter', sans-serif;
+ font-weight: 800;
+ font-style: italic;
+}
+
+.live-pinger {
+ width: .5em;
+ height: .5em;
+ margin: .1em .2em;
+ display: inline-block;
+ border-radius: 100%;
+ background-color: var(--primary);
+ box-shadow: 0 0 4px var(--primary);
+ animation: live-pinger-pulse 1s infinite alternate ease-in-out;
+}
+
+@keyframes live-pinger-pulse {
+ from {
+ opacity: .8;
+ transform: scale(1.0);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+}
+
.live-game {
overflow: hidden;
text-wrap: nowrap;
diff --git a/public/style/main.css b/public/style/main.css
index 4e9e113..680ee2b 100644
--- a/public/style/main.css
+++ b/public/style/main.css
@@ -3,6 +3,7 @@
@import url("/style/footer.css");
@import url("/style/prideflag.css");
@import url("/style/cursor.css");
+@import url("/font/inter/inter.css");
@font-face {
font-family: "Monaspace Argon";
diff --git a/view/index.go b/view/index.go
index 995de44..b6e3891 100644
--- a/view/index.go
+++ b/view/index.go
@@ -22,7 +22,7 @@ func IndexHandler(app *model.AppState) http.Handler {
var err error
var twitchStatus *model.TwitchStreamInfo = nil
- if len(app.Config.Twitch.Broadcaster) > 0 {
+ if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
if err != nil {
fmt.Fprintf(os.Stderr, "WARN: Failed to get Twitch status for %s: %v\n", app.Config.Twitch.Broadcaster, err)
diff --git a/views/index.html b/views/index.html
index 98a862c..6985f6c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -24,13 +24,13 @@
{{if .TwitchStatus}}
-
ari is LIVE right now!
+
ari melody LIVE
streaming: {{.TwitchStatus.GameName}}
{{.TwitchStatus.Title}}
{{.TwitchStatus.ViewerCount}} viewers
From 375ae84ae33d352421dffcfbe44bbbdff32a3b11 Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sun, 22 Jun 2025 18:00:08 +0100
Subject: [PATCH 48/60] update some arimelody.me references to .space
---
views/index.html | 30 ++++--------------------------
1 file changed, 4 insertions(+), 26 deletions(-)
diff --git a/views/index.html b/views/index.html
index 6985f6c..37bd2a3 100644
--- a/views/index.html
+++ b/views/index.html
@@ -6,8 +6,8 @@
-
-
+
+
@@ -101,28 +101,6 @@
where to find me 🛰️
-
-
+
-
+
diff --git a/views/music-gateway.html b/views/music-gateway.html
index 0f4441c..9007c02 100644
--- a/views/music-gateway.html
+++ b/views/music-gateway.html
@@ -6,22 +6,22 @@
-
+
-
+
-
-
+
+
-
+
@@ -34,7 +34,7 @@
-
<
+
<
diff --git a/views/music.html b/views/music.html
index 6f84672..51f712d 100644
--- a/views/music.html
+++ b/views/music.html
@@ -6,8 +6,8 @@
-
-
+
+
@@ -72,7 +72,7 @@
music used: ari melody - free2play
- https://arimelody.me/music/free2play
+ https://arimelody.space/music/free2play
licensed under CC BY-SA 4.0 .
From c63a090569d6ef8d12743f802b0361e22c4422cd Mon Sep 17 00:00:00 2001
From: ari melody
Date: Sat, 16 Aug 2025 22:35:49 +0100
Subject: [PATCH 55/60] fix HTTPLog panic with no User-Agent
---
main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/main.go b/main.go
index 53f2883..edd4c87 100644
--- a/main.go
+++ b/main.go
@@ -626,6 +626,6 @@ func HTTPLog(next http.Handler) http.Handler {
lrw.Status,
colour.Reset,
elapsed,
- r.Header["User-Agent"][0])
+ r.Header.Get("User-Agent"))
})
}
From 5a330ad7fa631ea331dbf02846262acabbd9ab3e Mon Sep 17 00:00:00 2001
From: ari melody
Date: Tue, 19 Aug 2025 15:22:59 +0100
Subject: [PATCH 56/60] fix some opengraph
---
views/index.html | 2 +-
views/music-gateway.html | 6 +++---
views/music.html | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/views/index.html b/views/index.html
index 23f40ea..23764e1 100644
--- a/views/index.html
+++ b/views/index.html
@@ -8,7 +8,7 @@
-
+
diff --git a/views/music-gateway.html b/views/music-gateway.html
index 9007c02..febef4d 100644
--- a/views/music-gateway.html
+++ b/views/music-gateway.html
@@ -9,8 +9,8 @@
-
-
+
+
@@ -19,7 +19,7 @@
-
+
diff --git a/views/music.html b/views/music.html
index 51f712d..e0a5110 100644
--- a/views/music.html
+++ b/views/music.html
@@ -8,7 +8,7 @@
-
+
From c82709084b32837956a86d2aa0ccdc4031f4dabf Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 20 Aug 2025 12:41:55 +0100
Subject: [PATCH 57/60] add quick security check to requests
---
main.go | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/main.go b/main.go
index edd4c87..9133958 100644
--- a/main.go
+++ b/main.go
@@ -515,7 +515,7 @@ func main() {
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
stdLog.Fatal(
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
- HTTPLog(DefaultHeaders(mux)),
+ CheckRequest(&app, HTTPLog(DefaultHeaders(mux))),
))
}
@@ -562,6 +562,37 @@ var PoweredByStrings = []string{
"30 billion dollars in VC funding",
}
+func CheckRequest(app *model.AppState, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // requests with empty user agents are considered suspicious.
+ // every browser supplies them; hell, even curl supplies them.
+ // i only ever see null user-agents paired with malicious requests,
+ // so i'm canning them altogether.
+ if len(r.Header.Get("User-Agent")) == 0 {
+ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+ return
+ }
+
+ // same with .php and awkward double-slash requests.
+ // obviously these don't affect me, but these tend to be lazy intrusion
+ // attempts. if that's what you're about, i don't want you on my site.
+ if strings.HasPrefix(r.URL.Path, "//") ||
+ strings.HasSuffix(r.URL.Path, ".php") ||
+ strings.HasSuffix(r.URL.Path, ".php7") {
+ http.NotFound(w, r)
+ fmt.Fprintf(
+ os.Stderr,
+ "WARN: Suspicious activity blocked: {\"path\":\"%s\",\"address\":\"%s\"}\n",
+ r.URL.Path,
+ r.RemoteAddr,
+ )
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
+
func DefaultHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Server", "ari melody webbed site")
From d13cfc74ad7c1f8d68c11adeeaf633fae38f9f4e Mon Sep 17 00:00:00 2001
From: ari melody
Date: Fri, 22 Aug 2025 01:06:37 +0100
Subject: [PATCH 58/60] =?UTF-8?q?complete=20arimelody.space=20migration!?=
=?UTF-8?q?=20=F0=9F=8E=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 11 +---
docker-compose.example.yml | 2 +-
public/keys/ari melody_0x92678188_public.asc | 26 --------
public/keys/ari@arimelody.space_public.asc | 66 ++++++++++++++++++++
views/index.html | 4 +-
views/music.html | 2 +-
6 files changed, 73 insertions(+), 38 deletions(-)
delete mode 100644 public/keys/ari melody_0x92678188_public.asc
create mode 100644 public/keys/ari@arimelody.space_public.asc
diff --git a/README.md b/README.md
index 75b2095..f1fd392 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,9 @@ home to your local SPACEGIRL! 💫
---
-built up from the initial [static](https://forge.arimelody.space/ari/arimelody.me-static)
-branch, this powerful, server-side rendered version comes complete with live
-updates, powered by a new database and handy admin panel!
-
-the admin panel currently facilitates live updating of my music discography,
-though i plan to expand it towards art portfolio and blog posts in the future.
-if all goes well, i'd like to later separate these components into their own
-library for others to use in their own sites. exciting stuff!
+a slightly-overcomplicated webserver built to show off everything i've worked
+on, and then some! this server comes complete with twitch live status tracking,
+a portfolio database, and a full-fledged admin CMS panel to manage it all!
## build
diff --git a/docker-compose.example.yml b/docker-compose.example.yml
index 62843b9..5ba8cfa 100644
--- a/docker-compose.example.yml
+++ b/docker-compose.example.yml
@@ -1,6 +1,6 @@
services:
web:
- image: docker.arimelody.space/arimelody.me:latest
+ image: docker.arimelody.space/arimelody-web:latest
build: .
ports:
- 8080:8080
diff --git a/public/keys/ari melody_0x92678188_public.asc b/public/keys/ari melody_0x92678188_public.asc
deleted file mode 100644
index 80a4676..0000000
--- a/public/keys/ari melody_0x92678188_public.asc
+++ /dev/null
@@ -1,26 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
-JriRCZy0HWFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkubWU+iJkEExYKAEECGwMF
-CwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AWIQTujeuNYocuegkeKt/PmYKckmeB
-iAUCZ7UqUAUJCIMP8wAKCRDPmYKckmeBiO/NAP0SoJL4aKZqCeYiSoDF/Uw6nMmZ
-+oR1Uig41wQ/IDbhCAEApP2vbjSIu6pcp0AQlL7qcoyPWv+XkqPSFqW9KEZZVwqI
-kwQTFgoAOxYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJk1bTdAhsDBQsJCAcCAiIC
-BhUKCQgLAgQWAgMBAh4HAheAAAoJEM+ZgpySZ4GIYJsA/jBNwsJTlmV9JMmsW0aF
-ApYDoPG1Q7sJ6CRW7xKCRjcqAQDX9iqNnW9Jqo8M3jXfu+aGSF926hg6M3SKm02P
-f27bAbgzBGe1JooWCSsGAQQB2kcPAQEHQJbfh5iLHEpZndMgekqYzqTrUoAJ8ZIL
-d4WH0dcw9tOaiPUEGBYKACYCGwIWIQTujeuNYocuegkeKt/PmYKckmeBiAUCZ7Uq
-VgUJBaOeTACBdiAEGRYKAB0WIQQlu5dWmBR/P3ZxngxgtfA4bj3bfgUCZ7UmigAK
-CRBgtfA4bj3bfux+AP4y5ydrjnGBMX7GuB2nh55SRdscSiXsZ66ntnjXyQcbWgEA
-pDuu7FqXzXcnluuZxNFDT740Rnzs60tTeplDqGGWcAQJEM+ZgpySZ4GIc0kA/iSw
-Nw+r3FC75omwrPpJF13B5fq93FweFx+oSaES6qzkAQDvgCK77qKKbvCju0g8zSsK
-EZnv6xR4uvtGdVkvLpBdC7gzBGe1JpkWCSsGAQQB2kcPAQEHQGnU4lXFLchhKYkC
-PshP+jvuRsNoedaDOK2p4dkQC8JuiH4EGBYKACYCGyAWIQTujeuNYocuegkeKt/P
-mYKckmeBiAUCZ7UqXgUJBaOeRQAKCRDPmYKckmeBiL9KAQCJZIBhuSsoYa61I0XZ
-cKzGZbB0h9pD6eg1VRswNIgHtQEAwu9Hgs1rs9cySvKbO7WgK6Qh6EfrvGgGOXCO
-m3wVsg24OARntSo5EgorBgEEAZdVAQUBAQdA+/k586W1OHxndzDJNpbd+wqjyjr0
-D5IXxfDs00advB0DAQgHiH4EGBYKACYWIQTujeuNYocuegkeKt/PmYKckmeBiAUC
-Z7UqOQIbDAUJBaOagAAKCRDPmYKckmeBiEFxAQCgziQt2l3u7jnZVij4zop+K2Lv
-TVFtkbG61tf6brRzBgD/X6c6X5BRyQC51JV1I1RFRBdeMAIXzcLFg2v3WUMccQs=
-=YmHI
------END PGP PUBLIC KEY BLOCK-----
diff --git a/public/keys/ari@arimelody.space_public.asc b/public/keys/ari@arimelody.space_public.asc
new file mode 100644
index 0000000..4323eba
--- /dev/null
+++ b/public/keys/ari@arimelody.space_public.asc
@@ -0,0 +1,66 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
+JriRCZy0IGFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkuc3BhY2U+iQJJBBMWCgHx
+AhsDBQkIgw/zBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBFiEE7o3rjWKH
+LnoJHirfz5mCnJJngYgFAmino5w1FIAAAAAAEAAccHJvb2ZAYXJpYWRuZS5pZGRu
+czphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFQ6FIAAAAAAEAAhcHJvb2ZAYXJpYWRu
+ZS5pZGh0dHBzOi8vZmVkaS5hcmltZWxvZHkuc3BhY2UvQGFyaUQUgAAAAAAQACtw
+cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3JnZS5ibGlzcy50b3duL2FyaS9rZXlv
+eGlkZS1wcm9vZkkUgAAAAAAQADBwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3Jn
+ZS5hcmltZWxvZHkuc3BhY2UvYXJpL2tleW94aWRlLXByb29mRhSAAAAAABAALXBy
+b29mQGFyaWFkbmUuaWRodHRwczovL2NvZGViZXJnLm9yZy9hcmltZWxvZHkva2V5
+b3hpZGUtcHJvb2ZlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vYnNr
+eS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6cnk1dW16a3hyMy9w
+b3N0LzNsaWlucW90cXRjMjIACgkQz5mCnJJngYjDpQEAgFn3bXcxw3xF0dwrSURh
+qpciMY31bkQy9eDMSKcbloIA/1hX1MnUKETdiAtrrK08z4udIXaJr52E5D7IAZk1
+pZwBtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
+jWKHLnoJHirfz5mCnJJngYgFAmTVtN0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC
+HgcCF4AACgkQz5mCnJJngYhgmwD+ME3CwlOWZX0kyaxbRoUClgOg8bVDuwnoJFbv
+EoJGNyoBANf2Ko2db0mqjwzeNd+75oZIX3bqGDozdIqbTY9/btsBiQIKBBMWCgGy
+AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJ
+Hirfz5mCnJJngYgFAme1wUA2FIAAAAAAEAAdcHJvb2ZAYXJpYWRuZS5pZGh0dHBz
+Oi8vaWNlLmFyaW1lbG9keS5tZS9AYXJpWxSAAAAAABAAQnByb29mQGFyaWFkbmUu
+aWRodHRwczovL2dpc3QuZ2l0aHViLmNvbS9hcmltZWxvZHkvMzY2ZGMyYjZhYWVk
+ZWMxOWU2MTRiN2NlY2U5Yzg2OWQyFIAAAAAAEAAZcHJvb2ZAYXJpYWRuZS5pZGRu
+czphcmltZWxvZHkubWU/dHlwZT1UWFRlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5p
+ZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6
+cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjJEFIAAAAAAEAArcHJvb2ZAYXJp
+YWRuZS5pZGh0dHBzOi8vZ2l0LmFyaW1lbG9keS5tZS9hcmkva2V5b3hpZGVfcHJv
+b2YACgkQz5mCnJJngYh3+QD+Pbo3bM4oWtUicGUGEp4jiFoBqSNlyl9rFPY0ODDS
+DxEBANaXz/No/Hn3mEwNdrFigj/YPm7TH/4UBbHAxN6hDggPiQJGBBMWCgHuAhsD
+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJHirf
+z5mCnJJngYgFAmino5VJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8v
+Zm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9vZkYUgAAAAAAQ
+AC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvYXJpbWVsb2R5
+L2tleW94aWRlLXByb29mOhSAAAAAABAAIXByb29mQGFyaWFkbmUuaWRodHRwczov
+L2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0BhcmllFIAAAAAAEABMcHJvb2ZAYXJpYWRu
+ZS5pZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBu
+Z2l6cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjI1FIAAAAAAEAAccHJvb2ZA
+YXJpYWRuZS5pZGRuczphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFREFIAAAAAAEAAr
+cHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3MudG93bi9hcmkva2V5
+b3hpZGUtcHJvb2YACgkQz5mCnJJngYgKNQD/UA2THttICUvz2p5cbPlJIm/QStRE
+6crttsTeFSsyocgBAPDXpkdssPNNnxxVvCNATTTxiS08Cy+xxQVrjWztjlUCuDME
+Z7UmihYJKwYBBAHaRw8BAQdAlt+HmIscSlmd0yB6SpjOpOtSgAnxkgt3hYfR1zD2
+05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSpWBQkFo55M
+AIF2IAQZFgoAHRYhBCW7l1aYFH8/dnGeDGC18DhuPdt+BQJntSaKAAoJEGC18Dhu
+Pdt+7H4A/jLnJ2uOcYExfsa4HaeHnlJF2xxKJexnrqe2eNfJBxtaAQCkO67sWpfN
+dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYhzSQD+JLA3D6vcULvm
+ibCs+kkXXcHl+r3cXB4XH6hJoRLqrOQBAO+AIrvuoopu8KO7SDzNKwoRme/rFHi6
++0Z1WS8ukF0LuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
+w2h51oM4ranh2RALwm6IfgQYFgoAJgIbIBYhBO6N641ihy56CR4q38+ZgpySZ4GI
+BQJntSpeBQkFo55FAAoJEM+ZgpySZ4GIv0oBAIlkgGG5KyhhrrUjRdlwrMZlsHSH
+2kPp6DVVGzA0iAe1AQDC70eCzWuz1zJK8ps7taArpCHoR+u8aAY5cI6bfBWyDbg4
+BGe1KjkSCisGAQQBl1UBBQEBB0D7+TnzpbU4fGd3MMk2lt37CqPKOvQPkhfF8OzT
+Rp28HQMBCAeIfgQYFgoAJhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSo5AhsM
+BQkFo5qAAAoJEM+ZgpySZ4GIQXEBAKDOJC3aXe7uOdlWKPjOin4rYu9NUW2RsbrW
+1/putHMGAP9fpzpfkFHJALnUlXUjVEVEF14wAhfNwsWDa/dZQxxxC7g4BGTVtN0S
+CisGAQQBl1UBBQEBB0CcDZ2s/NAGhc13AisWei+4XQKNf7z7xBK6AIXhrlkRcQMB
+CAeIeAQoFgoAIBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntT6fAh0DAAoJEM+Z
+gpySZ4GIgX8A/1d8CZFSRB0TRU8h6ijTS1+O2bKJ0uwydfQHL5b3fA4OAQDOU6eG
+Ml82IKGhbFoJl7wm5X4+l5+lNqwZymNoZjVhBIh4BBgWCgAgFiEE7o3rjWKHLnoJ
+Hirfz5mCnJJngYgFAmTVtN0CGwwACgkQz5mCnJJngYgv8QEA9YbuFnLLUeNJZFMT
+KoWeOMJos6wwPnhgnYexntxsu/cBAMd/ORp2KDaZTEwOAUxrO6K1eFkn0pKAcdPq
+cdVDnsIL
+=Mzcq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/views/index.html b/views/index.html
index 23764e1..6d0af2c 100644
--- a/views/index.html
+++ b/views/index.html
@@ -66,7 +66,7 @@
for anything else, you can reach me for any and all communications through
- ari@arimelody.me . if your message
+ ari@arimelody.space . if your message
contains anything beyond a silly gag, i strongly recommend encrypting
your message using my public pgp key, listed below!
@@ -93,7 +93,7 @@
my keys 🔑
diff --git a/views/music.html b/views/music.html
index e0a5110..e7b4bd9 100644
--- a/views/music.html
+++ b/views/music.html
@@ -84,7 +84,7 @@
if you do happen to use my work in something you're particularly proud of, feel free to send it my way!
- > ari@arimelody.me
+ > ari@arimelody.space
From 6bd0b5ec0e853d13af0f4b8ed7827d7944ee0efc Mon Sep 17 00:00:00 2001
From: ari melody
Date: Wed, 27 Aug 2025 18:42:30 +0100
Subject: [PATCH 59/60] update pgp key
---
public/keys/ari@arimelody.space_public.asc | 77 ++++++++++------------
1 file changed, 35 insertions(+), 42 deletions(-)
diff --git a/public/keys/ari@arimelody.space_public.asc b/public/keys/ari@arimelody.space_public.asc
index 4323eba..9e6fea2 100644
--- a/public/keys/ari@arimelody.space_public.asc
+++ b/public/keys/ari@arimelody.space_public.asc
@@ -2,19 +2,19 @@
mDMEZNW03RYJKwYBBAHaRw8BAQdAuMUNVjXT7m/YisePPnSYY6lc1Xmm3oS79ZEO
JriRCZy0IGFyaSBtZWxvZHkgPGFyaUBhcmltZWxvZHkuc3BhY2U+iQJJBBMWCgHx
-AhsDBQkIgw/zBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBFiEE7o3rjWKH
-LnoJHirfz5mCnJJngYgFAmino5w1FIAAAAAAEAAccHJvb2ZAYXJpYWRuZS5pZGRu
-czphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFQ6FIAAAAAAEAAhcHJvb2ZAYXJpYWRu
-ZS5pZGh0dHBzOi8vZmVkaS5hcmltZWxvZHkuc3BhY2UvQGFyaUQUgAAAAAAQACtw
-cm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3JnZS5ibGlzcy50b3duL2FyaS9rZXlv
-eGlkZS1wcm9vZkkUgAAAAAAQADBwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9mb3Jn
-ZS5hcmltZWxvZHkuc3BhY2UvYXJpL2tleW94aWRlLXByb29mRhSAAAAAABAALXBy
-b29mQGFyaWFkbmUuaWRodHRwczovL2NvZGViZXJnLm9yZy9hcmltZWxvZHkva2V5
-b3hpZGUtcHJvb2ZlFIAAAAAAEABMcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vYnNr
-eS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBuZ2l6cnk1dW16a3hyMy9w
-b3N0LzNsaWlucW90cXRjMjIACgkQz5mCnJJngYjDpQEAgFn3bXcxw3xF0dwrSURh
-qpciMY31bkQy9eDMSKcbloIA/1hX1MnUKETdiAtrrK08z4udIXaJr52E5D7IAZk1
-pZwBtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
+AhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAhkBNRSAAAAAABAAHHByb29m
+QGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9VFhUOhSAAAAAABAA
+IXByb29mQGFyaWFkbmUuaWRodHRwczovL2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0Bh
+cmlEFIAAAAAAEAArcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3Mu
+dG93bi9hcmkva2V5b3hpZGUtcHJvb2ZJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5p
+ZGh0dHBzOi8vZm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9v
+ZkYUgAAAAAAQAC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcv
+YXJpbWVsb2R5L2tleW94aWRlLXByb29mZRSAAAAAABAATHByb29mQGFyaWFkbmUu
+aWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBsYzp5Y3Q2Y3ZnZmlwbmdp
+enJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyFiEE7o3rjWKHLnoJHirfz5mC
+nJJngYgFAmivQJsFCQ0/jT4ACgkQz5mCnJJngYgdtQD+K8AMkLvR1ZKxl0tw8/FO
+vwS9HknEW13GajSAY/W1/NoA/17mnVnlTFhepKo1ETnxe2BpdOaKR85K0n2qffzC
+8SAAtB1hcmkgbWVsb2R5IDxhcmlAYXJpbWVsb2R5Lm1lPoiTBBMWCgA7FiEE7o3r
jWKHLnoJHirfz5mCnJJngYgFAmTVtN0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwEC
HgcCF4AACgkQz5mCnJJngYhgmwD+ME3CwlOWZX0kyaxbRoUClgOg8bVDuwnoJFbv
EoJGNyoBANf2Ko2db0mqjwzeNd+75oZIX3bqGDozdIqbTY9/btsBiQIKBBMWCgGy
@@ -29,38 +29,31 @@ cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjJEFIAAAAAAEAArcHJvb2ZAYXJp
YWRuZS5pZGh0dHBzOi8vZ2l0LmFyaW1lbG9keS5tZS9hcmkva2V5b3hpZGVfcHJv
b2YACgkQz5mCnJJngYh3+QD+Pbo3bM4oWtUicGUGEp4jiFoBqSNlyl9rFPY0ODDS
DxEBANaXz/No/Hn3mEwNdrFigj/YPm7TH/4UBbHAxN6hDggPiQJGBBMWCgHuAhsD
-BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheABQkIgw/zFiEE7o3rjWKHLnoJHirf
-z5mCnJJngYgFAmino5VJFIAAAAAAEAAwcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8v
-Zm9yZ2UuYXJpbWVsb2R5LnNwYWNlL2FyaS9rZXlveGlkZS1wcm9vZkYUgAAAAAAQ
-AC1wcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvYXJpbWVsb2R5
-L2tleW94aWRlLXByb29mOhSAAAAAABAAIXByb29mQGFyaWFkbmUuaWRodHRwczov
-L2ZlZGkuYXJpbWVsb2R5LnNwYWNlL0BhcmllFIAAAAAAEABMcHJvb2ZAYXJpYWRu
-ZS5pZGh0dHBzOi8vYnNreS5hcHAvcHJvZmlsZS9kaWQ6cGxjOnljdDZjdmdmaXBu
-Z2l6cnk1dW16a3hyMy9wb3N0LzNsaWlucW90cXRjMjI1FIAAAAAAEAAccHJvb2ZA
-YXJpYWRuZS5pZGRuczphcmltZWxvZHkuc3BhY2U/dHlwZT1UWFREFIAAAAAAEAAr
-cHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vZm9yZ2UuYmxpc3MudG93bi9hcmkva2V5
-b3hpZGUtcHJvb2YACgkQz5mCnJJngYgKNQD/UA2THttICUvz2p5cbPlJIm/QStRE
-6crttsTeFSsyocgBAPDXpkdssPNNnxxVvCNATTTxiS08Cy+xxQVrjWztjlUCuDME
+BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheASRSAAAAAABAAMHByb29mQGFyaWFk
+bmUuaWRodHRwczovL2ZvcmdlLmFyaW1lbG9keS5zcGFjZS9hcmkva2V5b3hpZGUt
+cHJvb2ZGFIAAAAAAEAAtcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vY29kZWJlcmcu
+b3JnL2FyaW1lbG9keS9rZXlveGlkZS1wcm9vZjoUgAAAAAAQACFwcm9vZkBhcmlh
+ZG5lLmlkaHR0cHM6Ly9mZWRpLmFyaW1lbG9keS5zcGFjZS9AYXJpZRSAAAAAABAA
+THByb29mQGFyaWFkbmUuaWRodHRwczovL2Jza3kuYXBwL3Byb2ZpbGUvZGlkOnBs
+Yzp5Y3Q2Y3ZnZmlwbmdpenJ5NXVtemt4cjMvcG9zdC8zbGlpbnFvdHF0YzIyNRSA
+AAAAABAAHHByb29mQGFyaWFkbmUuaWRkbnM6YXJpbWVsb2R5LnNwYWNlP3R5cGU9
+VFhURBSAAAAAABAAK3Byb29mQGFyaWFkbmUuaWRodHRwczovL2ZvcmdlLmJsaXNz
+LnRvd24vYXJpL2tleW94aWRlLXByb29mFiEE7o3rjWKHLnoJHirfz5mCnJJngYgF
+AmivQJsFCQ0/jT4ACgkQz5mCnJJngYhk/wEAuQMYpUgyLqcYvOh1A+7f/t+DUXjz
+YjQtLYw37oAESREA/074iJNi9GHGIjxYfp5lBkZxqGew1GAFIKx7Yzp64WQFuDME
Z7UmihYJKwYBBAHaRw8BAQdAlt+HmIscSlmd0yB6SpjOpOtSgAnxkgt3hYfR1zD2
-05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSpWBQkFo55M
+05qI9QQYFgoAJgIbAhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0AjBQkKYBsZ
AIF2IAQZFgoAHRYhBCW7l1aYFH8/dnGeDGC18DhuPdt+BQJntSaKAAoJEGC18Dhu
Pdt+7H4A/jLnJ2uOcYExfsa4HaeHnlJF2xxKJexnrqe2eNfJBxtaAQCkO67sWpfN
-dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYhzSQD+JLA3D6vcULvm
-ibCs+kkXXcHl+r3cXB4XH6hJoRLqrOQBAO+AIrvuoopu8KO7SDzNKwoRme/rFHi6
-+0Z1WS8ukF0LuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
+dyeW65nE0UNPvjRGfOzrS1N6mUOoYZZwBAkQz5mCnJJngYgc2gD/cFhjrwPdex9g
+ZYk7jH29wQ9RpR9dEhf0C20nFZJLawgBAOBbzw4/O7OslSoIjhGs4pw9hJIBK7ds
+PI6g3CeX0DUFuDMEZ7UmmRYJKwYBBAHaRw8BAQdAadTiVcUtyGEpiQI+yE/6O+5G
w2h51oM4ranh2RALwm6IfgQYFgoAJgIbIBYhBO6N641ihy56CR4q38+ZgpySZ4GI
-BQJntSpeBQkFo55FAAoJEM+ZgpySZ4GIv0oBAIlkgGG5KyhhrrUjRdlwrMZlsHSH
-2kPp6DVVGzA0iAe1AQDC70eCzWuz1zJK8ps7taArpCHoR+u8aAY5cI6bfBWyDbg4
+BQJor0AjBQkKYBsKAAoJEM+ZgpySZ4GIoMAA/jB/exnjGvsMKuNW09bI29bsKHNW
+SQLjEnuuByN6Spq6AP9yPUumsSEHr0W71iefMuNFJZnF+8qSk+uywQ/5ET+PBbg4
BGe1KjkSCisGAQQBl1UBBQEBB0D7+TnzpbU4fGd3MMk2lt37CqPKOvQPkhfF8OzT
-Rp28HQMBCAeIfgQYFgoAJhYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntSo5AhsM
-BQkFo5qAAAoJEM+ZgpySZ4GIQXEBAKDOJC3aXe7uOdlWKPjOin4rYu9NUW2RsbrW
-1/putHMGAP9fpzpfkFHJALnUlXUjVEVEF14wAhfNwsWDa/dZQxxxC7g4BGTVtN0S
-CisGAQQBl1UBBQEBB0CcDZ2s/NAGhc13AisWei+4XQKNf7z7xBK6AIXhrlkRcQMB
-CAeIeAQoFgoAIBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJntT6fAh0DAAoJEM+Z
-gpySZ4GIgX8A/1d8CZFSRB0TRU8h6ijTS1+O2bKJ0uwydfQHL5b3fA4OAQDOU6eG
-Ml82IKGhbFoJl7wm5X4+l5+lNqwZymNoZjVhBIh4BBgWCgAgFiEE7o3rjWKHLnoJ
-Hirfz5mCnJJngYgFAmTVtN0CGwwACgkQz5mCnJJngYgv8QEA9YbuFnLLUeNJZFMT
-KoWeOMJos6wwPnhgnYexntxsu/cBAMd/ORp2KDaZTEwOAUxrO6K1eFkn0pKAcdPq
-cdVDnsIL
-=Mzcq
+Rp28HQMBCAeIfgQYFgoAJgIbDBYhBO6N641ihy56CR4q38+ZgpySZ4GIBQJor0Ak
+BQkKYBdqAAoJEM+ZgpySZ4GIlIoA/0fv2UQyhixu7Vkq7IeQ+NxUuEVCIGmrAu6k
+ScT13ikjAQCPpIubU848yXcDUvxgcGAS7yNADU1dAWAZOi34WxajAQ==
+=caf3
-----END PGP PUBLIC KEY BLOCK-----
From 1eed8f0e84eeb8fabcc6a19ff0d717d63622431a Mon Sep 17 00:00:00 2001
From: ari melody
Date: Fri, 29 Aug 2025 16:29:28 +0100
Subject: [PATCH 60/60] day 1 patch
---
main.go | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/main.go b/main.go
index 9133958..f3c9f66 100644
--- a/main.go
+++ b/main.go
@@ -33,6 +33,7 @@ import (
const DB_VERSION = 1
const DEFAULT_PORT int64 = 8080
+const HRT_DATE int64 = 1756478697
func main() {
fmt.Printf("made with <3 by ari melody\n\n")
@@ -605,6 +606,10 @@ func DefaultHeaders(next http.Handler) http.Handler {
"X-Powered-By",
PoweredByStrings[rand.Intn(len(PoweredByStrings))],
)
+ w.Header().Add(
+ "X-Days-Since-HRT",
+ fmt.Sprint(math.Round(time.Since(time.Unix(HRT_DATE, 0)).Hours() / 24)),
+ )
next.ServeHTTP(w, r)
})
}