small pixel drawing of a pufferfish j3s.sh

main.go

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"io"
	"log"
	"math/rand"
	"net/http"
	"os"
	"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"
	"github.com/SlyMarbo/rss"
)

// 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 templateData struct {
	Feeds        []rss.Feed
	CurrentBooks *openlibrary.CurrentBooks
}

var data templateData

// the populate function populates the global "data" variable
// with ... data. with which to pass into templates.
func (t *templateData) 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)
	}
}

// this is here basically because /favicon.ico needs to be
// redirected to /static/favicon.ico
func faviconHandler(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "static/favicon.ico")
}

func main() {
	go data.populate()

	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", http.StripPrefix("/static/", 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("/favicon.ico", faviconHandler)
	http.HandleFunc("/feeds.html", feedHandler)
	http.HandleFunc("/feed.atom", atom.Handler)
	http.HandleFunc("/age.html", ageHandler)
	http.HandleFunc("/ip.html", ipHandler)
	http.HandleFunc("/review/", postHandler)
	http.HandleFunc("/thought/", postHandler)
	http.HandleFunc("/", serveRoot)

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

// templatesCombobulator takes a template & lays it on top
// of the layout template, then passes it back to the user
// this thing also sprinkles on some x-tra necessary funcs
func templateCombobulator(requestedPath string) (template.Template, error) {
	layoutPath := filepath.Join("templates", "layout.html")

	_, err := os.Stat(requestedPath)
	if err != nil {
		return template.Template{}, err
	}

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

	tmpl, err := template.New("unimportant").Funcs(funcMap).ParseFiles(layoutPath, requestedPath)
	if err != nil {
		return *tmpl, err
	}
	return *tmpl, nil
}

func serveRoot(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		r.URL.Path = "index.html"
	}
	requestedPath := filepath.Join("templates", filepath.Clean(r.URL.Path))

	tmpl, err := templateCombobulator(requestedPath)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(404), 404)
		return
	}

	err = tmpl.ExecuteTemplate(w, "layout", data)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}

func postHandler(w http.ResponseWriter, r *http.Request) {
	lp := filepath.Join("templates", "simple-layout.html")
	fp := filepath.Join(strings.TrimPrefix(filepath.Clean(r.URL.Path), "/"))

	info, err := os.Stat(fp)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
	}

	if info.IsDir() {
		http.NotFound(w, r)
		return
	}

	content, err := os.ReadFile(fp)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
		return
	}

	tmpl, err := template.ParseFiles(lp)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
		return
	}

	err = tmpl.ExecuteTemplate(w, "simple-layout", string(content))
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}

type AgeForm struct {
	Success   bool
	PublicKey string
	Message   string
}

func ageHandler(w http.ResponseWriter, r *http.Request) {
	tmpl, err := templateCombobulator(filepath.Join("templates", "age.html"))
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
		return
	}

	if r.Method != http.MethodPost {
		tmpl.ExecuteTemplate(w, "layout", nil)
		return
	}

	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)
	}

	err = tmpl.ExecuteTemplate(w, "layout", buf.String())
	if err != nil {
		log.Println(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}

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>j3s's rss feeds</title>
</head>
<body>
<table>
<tbody>`)

	posts := feed.SortItems(data.Feeds)
	for _, p := range posts {
		fmt.Fprintf(w, "<tr><td>")
		fmt.Fprintf(w, `<a href="%s">%s</a>`, p.Link, p.Title)
		fmt.Fprintf(w, "</tr></td>")
	}
}

func randomRotationDegrees() int {
	rand.Seed(time.Now().UnixNano())
	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]
}