small pixel drawing of a pufferfish j3s.sh

main.go

package main

import (
	"bytes"
	"embed"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"log"
	"math/rand"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"filippo.io/age"
	"filippo.io/age/armor"
	"git.j3s.sh/j3s.sh/atom"
	"git.j3s.sh/j3s.sh/feed"
	"git.j3s.sh/j3s.sh/openlibrary"
	"git.j3s.sh/j3s.sh/thought"
	"github.com/SlyMarbo/rss"
)

//go:embed templates
var templateFiles embed.FS

//go:embed static
var staticFiles embed.FS

// templateData is a mega-struct that gets
// passed to every single template - put whatever
// you want in it tbh.
//
// "data" is a global object that contains arbitrary
// data for use in templates. it's useful for it to be
// global since it may need to be available in arbitrary
// contexts. maybe that sucks. but idfk!
type templateDataStruct struct {
	Feeds        []rss.Feed
	CurrentBooks *openlibrary.CurrentBooks
}

var templateData templateDataStruct

// the populate function populates the global "data" variable
// with ... data. with which to pass into templates.
func (t *templateDataStruct) populate() {
	// call for books first bc it's fast
	t.CurrentBooks = openlibrary.GetCurrentBooks()
	t.Feeds = feed.GetAllFeeds()
	// refresh all feeds periodically idk 60 mins seems
	// like a fine-ass interval
	for range time.Tick(time.Minute * 60) {
		log.Println("updating fetched objects")
		t.CurrentBooks = openlibrary.GetCurrentBooks()
		for i := range t.Feeds {
			err := t.Feeds[i].Update()
			if err != nil {
				log.Printf("%s: %s\n", t.Feeds[i].Title, err)
			}
		}
	}
}

func redirHandler(url string) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, url, 302)
	}
}

func main() {
	go templateData.populate()

	fs, err := fs.Sub(staticFiles, "static")
	if err != nil {
		log.Fatal(err)
	}
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(fs))))

	// redirs up front - some of these are here for
	// historic reasons, others for utility
	http.HandleFunc("/ip", redirHandler("/ip.html"))
	http.HandleFunc("/feeds", redirHandler("/feeds.html"))
	http.HandleFunc("/tools.html", redirHandler("/creations.html"))
	http.HandleFunc("/projects.html", redirHandler("/creations.html"))

	http.HandleFunc("/feeds.html", feedHandler)
	http.HandleFunc("/feed.atom", atom.Handler)
	http.HandleFunc("/age.html", ageHandler)
	http.HandleFunc("/ip.html", ipHandler)
	// TODO: redir to https://abyss.j3s.sh
	// http.HandleFunc("/review/", postHandler)
	http.HandleFunc("/thought/", thoughtHandler)
	http.HandleFunc("/", serveRoot)

	log.Println("listening on localhost:4666")
	err = http.ListenAndServe(":4666", nil)
	if err != nil {
		log.Fatal(err)
	}
}

// renderPage renders the given page and passes data to the
// template execution engine. it's normally the last thing a
// handler should do tbh.
//
// pass a struct in data & it will be available in your page
// via ".Data"
func renderPage(w http.ResponseWriter, r *http.Request, page string, data any) {
	// sanitizing some sheet
	page = filepath.Clean(page)
	page = strings.TrimPrefix(page, "/")

	funcMap := template.FuncMap{
		"toLower":               strings.ToLower,
		"randomheaderphrase":    randomJesPhrase,
		"randomrotationdegrees": randomRotationDegrees,
	}

	tmpl, err := template.New("").Funcs(funcMap).ParseFS(templateFiles, "templates/*.html")
	if err != nil {
		log.Println(err)
		http.Error(w, err.Error(), 500)
		return
	}

	if tmpl.Lookup(page) == nil {
		http.NotFound(w, r)
		return
	}

	pageData := struct {
		Title string
		Data  any
	}{
		Data: data,
	}

	err = tmpl.ExecuteTemplate(w, page, pageData)
	if err != nil {
		log.Println(err)
		http.Error(w, err.Error(), 500)
		return
	}
}

func serveRoot(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		r.URL.Path = "index.html"
	}

	renderPage(w, r, r.URL.Path, templateData)
}

func thoughtHandler(w http.ResponseWriter, r *http.Request) {
	post, err := thought.Post(path.Base(r.URL.Path))
	if err != nil {
		log.Println(err)
		if os.IsNotExist(err) {
			http.NotFound(w, r)
		} else {
			http.Error(w, err.Error(), 500)
		}
		return
	}

	renderPage(w, r, "thought.html", post)
}

func ageHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		renderPage(w, r, "age.html", nil)
	}
	if r.Method == "POST" {
		type AgeForm struct {
			Success   bool
			PublicKey string
			Message   string
		}
		formdeets := AgeForm{
			PublicKey: r.FormValue("pubkey"),
			Message:   r.FormValue("message"),
		}

		recipient, err := age.ParseX25519Recipient(formdeets.PublicKey)
		if err != nil {
			log.Println(err.Error())
			http.Error(w, http.StatusText(500), 500)
			return
		}

		buf := &bytes.Buffer{}
		armorWriter := armor.NewWriter(buf)

		write, err := age.Encrypt(armorWriter, recipient)
		if err != nil {
			log.Println(err.Error())
			http.Error(w, http.StatusText(500), 500)
			return
		}
		if _, err := io.WriteString(write, formdeets.Message); err != nil {
			log.Println(err.Error())
			http.Error(w, http.StatusText(500), 500)
			return
		}
		if err := write.Close(); err != nil {
			log.Println(err.Error())
			http.Error(w, http.StatusText(500), 500)
			return
		}
		if err := armorWriter.Close(); err != nil {
			log.Fatalf("Failed to close armor: %v", err)
		}

		renderPage(w, r, "age.html", buf.String())
	}
}

func ipHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	// this header is always set by tlstunnel (MAYBE)
	// so we can PROBABLY expect it to be here IDFK RLY
	// IT'S LATE AND IM TIRED
	ip := r.Header.Get("X-Forwarded-For")
	if ip != "" {
		fmt.Fprintf(w, r.Header.Get("X-Forwarded-For")+"\n")
	} else {
		fmt.Fprintf(w, "no ip detected\n")
	}
}

func feedHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, `<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>feeds have moved!!</title>
</head>
<p> j3s's feeds have moved to <a href="https://vore.website/j3s">vore.website</a>`)
}

func randomRotationDegrees() int {
	return rand.Intn(500 - -500) + -500
}

func randomJesPhrase() string {
	wordList := []string{
		"shit hell",
		"loves shell",
		"is manic",
		"hugs you",
		"*fucking dies*",
		"logs off",
		"closes the world",
		"jes jes jes jes jes",
		"is a mess",
		"stands like a rabbit",
		"walks to waterfall",
		"eats rice pudding",
		"devours noodles",
		"walks 1 mile",
		"plays dota2",
		"blinks slowly",
		"inspires ideas",
		"hates jump king",
		"<3",
		"pets every cat",
		"is a 0.5 on the binary",
		"winks",
		"has social anxiety",
		"tilts under pressure",
		"simply passes away",
		"den",
		"is a skeletor",
		"listens thoughtfully",
	}
	wordIndex := rand.Intn(len(wordList))
	return wordList[wordIndex]
}