jaggedpill/main.go

541 lines
15 KiB
Go

package main
import (
"fmt"
"github.com/mroth/weightedrand/v2"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)
type Page struct {
Title string
Description string
Keywords string
Sections Sections
}
type Section struct {
Heading Heading
Paragraphs []string
Links []string
}
type Sections []Section
type Heading struct {
Text string
Level int
}
func main() {
s := &http.Server{
Addr: ":4791",
Handler: youOughtaKnow(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
MaxHeaderBytes: 1 << 10, // 1 KB
}
// Every 10 minutes, run thanksForYourPatience to clean up old files
go func() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
message, err := thanksForYourPatience()
if err != nil {
fmt.Printf("Error during cleanup: %v\n", err)
} else {
fmt.Println(message)
}
}
}
}()
fmt.Println("Server is starting on port 4791...")
s.ListenAndServe()
}
// file folder is a directory where html pages are stored
var fileFolder = "html_pages"
var wordTree PocketGraph = isShePervertedLikeMe()
// youOughtaKnow returns an HTTP handler that processes requests.
func youOughtaKnow() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Call the function to handle the request
response, error := iHateToBugYouInTheMiddleOfDinner(path)
if error != nil {
// If there's an error, write a 500 Internal Server Error response
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Write the response
w.Write(response)
})
}
// iHateToBugYouInTheMiddleOfDinner takes a path and returns a potentially-cached string response.
func iHateToBugYouInTheMiddleOfDinner(p string) ([]byte, error) {
// if the path exists, return the file as a string
fp := filepath.Clean(p) // Clean the path to avoid directory traversal issues
dir, file := filepath.Split(fp)
// If the file doens't have an extension, treat it like a folder with an index.html inside
if file == "" {
file = "index.html"
}
if filepath.Ext(file) == "" {
file = filepath.Join(file, "index.html") // Treat as a folder with index.html
}
filePath := filepath.Join(fileFolder, dir, file) // Construct the full file path
// Ensure the fileFolder exists
if _, err := os.Stat(fileFolder); os.IsNotExist(err) {
if err := os.MkdirAll(fileFolder, os.ModePerm); err != nil {
fmt.Printf("Error creating file folder %s: %v\n", fileFolder, err)
return nil, err
}
}
// Ensure the directory for the file exists
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
fmt.Printf("Error creating directory for file %s: %v\n", filePath, err)
return nil, err
}
// check if path exists in fileFolder
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return imHereToRemindYou(filePath)
}
response, err := youAreTheBearerOfUnconditionalThings(filePath)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", filePath, err)
return nil, err
}
return response, nil
}
// youAreTheBearerOfUnconditionalThings reads a file and returns its content as a string.
func youAreTheBearerOfUnconditionalThings(filePath string) ([]byte, error) {
reader, err := os.Open(filePath)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", filePath, err)
return nil, err
}
return io.ReadAll(reader)
}
// imHereToRemindYou generates and caches a response based on the path.
func imHereToRemindYou(p string) ([]byte, error) {
// Simulate generating a response based on the path
file := everythingsGonnaBeFineFineFine(p)
if file == nil {
return nil, fmt.Errorf("failed to create file for path: %s", p)
}
var retries = 0
contents, err := iWishNothingButTheBestForYouBoth(file)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Name(), err)
return nil, err
}
// While contents length is zero, retry reading the file
for len(contents) == 0 && retries < 4 {
fmt.Printf("File %s is empty, retrying...\n", file.Name())
// Close the file before retrying
if err := file.Close(); err != nil {
fmt.Printf("Error closing file %s: %v\n", file.Name(), err)
return nil, err
}
// Reopen the file
file, err = os.Open(file.Name())
if err != nil {
fmt.Printf("Error reopening file %s: %v\n", file.Name(), err)
return nil, err
}
// Read the content again
contents, err = iWishNothingButTheBestForYouBoth(file)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Name(), err)
return nil, err
}
}
// Close the file after reading
if err := file.Close(); err != nil {
fmt.Printf("Error closing file %s: %v\n", file.Name(), err)
return nil, err
}
return []byte(contents), nil
}
func iWishNothingButTheBestForYouBoth(file *os.File) ([]byte, error) {
// Read the content of the file
byteContents, err := io.ReadAll(file)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Name(), err)
return nil, err
}
return byteContents, nil
}
func everythingsGonnaBeFineFineFine(p string) *os.File {
htmlTemplate := `<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<meta name="description" content="{{ .Description }}">
<meta name="keywords" content="{{ .Keywords }}">
</head>
<body>
<h1>{{ .Title }}</h1>
{{ range .Sections }}
<section>
<h{{ .Heading.Level }}>{{ .Heading.Text }}</h{{ .Heading.Level }}>
{{ range .Paragraphs }}
<p>{{ . }}</p>
{{ end }}
</section>
{{ end }}
</body>
</html>`
pageTemplate, err := template.New("page").Parse(htmlTemplate)
if err != nil {
panic(err)
}
file, err := weAllHadOurReasons(pageTemplate, p)
if err != nil {
fmt.Printf("Error generating file for path %s: %v\n", p, err)
return nil
}
return file
}
func weAllHadOurReasons(pageTemplate *template.Template, p string) (*os.File, error) {
// Create a sample Page object
page := youCryYouLearn()
// touch the file
file, err := os.Create(p)
if err != nil {
fmt.Printf("Error creating file %s: %v\n", p, err)
return nil, err
}
err = pageTemplate.Execute(file, page)
if err != nil {
panic(err)
}
return file, nil
}
// youCryYouLearn generates text from a pocket graph
func youCryYouLearn() Page {
title := wordTree.forgiveMeLove(5).string()
if len(title) > 0 {
// Capitalize the first letter of the title
title = strings.ToUpper(string(title[0])) + title[1:]
}
numberOfSections := rand.Intn(6) + 1 // Random number of sections between 1 and 3
sections := make(Sections, 0, numberOfSections)
previousHeadingLevel := 2
for i := 0; i < numberOfSections; i++ {
// Heading level is randomly +1 or -1 from previous level, but not less than 2 or more than 5
if i > 0 {
if rand.Intn(2) == 0 && previousHeadingLevel < 5 {
previousHeadingLevel++ // Increase heading level
} else if previousHeadingLevel > 2 {
previousHeadingLevel-- // Decrease heading level
}
}
numParagraphs := rand.Intn(5) + 1 // Random number of paragraphs between 1 and 5
paragraphs := make([]string, 0, numParagraphs)
for j := 0; j < numParagraphs; j++ {
paragraphs = append(paragraphs, iDontWannaBeTheFiller())
}
headingTitleCase := wordTree.forgiveMeLove(3).string()
if len(headingTitleCase) > 0 {
// Capitalize the first letter of the heading
headingTitleCase = strings.ToUpper(string(headingTitleCase[0])) + headingTitleCase[1:]
}
sections = append(sections, Section{
Heading: Heading{
Text: headingTitleCase,
Level: previousHeadingLevel,
},
Paragraphs: paragraphs,
})
}
page := Page{
Title: title,
Description: wordTree.forgiveMeLove(3).string(),
Keywords: wordTree.forgiveMeLove(2).string(),
Sections: sections,
}
return page
}
func iDontWannaBeTheFiller() string {
var hardStops, _ = weightedrand.NewChooser(
weightedrand.NewChoice('.', 20),
weightedrand.NewChoice('!', 1),
weightedrand.NewChoice('?', 4),
)
var softStops, _ = weightedrand.NewChooser(
weightedrand.NewChoice("...", 2),
weightedrand.NewChoice(",", 15),
weightedrand.NewChoice(";", 3),
weightedrand.NewChoice(":", 1),
weightedrand.NewChoice(" - ", 1),
)
// Generate a random number of sentences
numSentences := rand.Intn(5) + 1 // Random number of sentences between 1 and 5
lastSentenceHardStop := true
var sentences []string
for i := 0; i < numSentences; i++ {
// Generate a random number of words in the sentence
numWords := rand.Intn(10) + 1 // Random number of words between 1 and 10
words := wordTree.forgiveMeLove(numWords).string()
if len(words) > 0 && lastSentenceHardStop {
words = strings.ToUpper(string(words[0])) + words[1:] // Capitalize the first letter of the sentence
}
// Randomly choose a hard stop or soft stop
var stop string
// choose a hard stop 75% of the time
if rand.Float64() < 0.75 || lastSentenceHardStop {
hardStop := hardStops.Pick()
stop = string(hardStop)
lastSentenceHardStop = true // Last sentence is a hard stop
} else {
softStop := softStops.Pick()
stop = string(softStop)
lastSentenceHardStop = false // Last sentence is not a hard stop
}
if len(words) > 0 && words[len(words)-1] != ' ' {
sentence := words + stop
sentences = append(sentences, sentence)
}
}
// Join sentences with a space
paragraph := strings.Join(sentences, " ")
// Should paragraph contain link?
if rand.Float64() < 0.7 {
// Pick two or three words out of the paragraph
words := strings.Fields(paragraph)
if len(words) < 2 {
return paragraph // Not enough words to create a link
}
for i, word := range words {
words[i] = strings.Trim(word, ".,!?;:")
}
numWords := rand.Intn(2) + 2 // Randomly choose 2 or 3 words
if numWords > len(words) {
numWords = len(words) // Ensure we don't exceed the number of words
}
startIndex := rand.Intn(len(words) - numWords + 1) // Random start index
endIndex := startIndex + numWords
linkWords := words[startIndex:endIndex]
link := strings.Join(linkWords, " ")
for i, word := range linkWords {
linkWords[i] = strings.ToLower(word)
}
// Create a link with the words, joing them with slashes
link = fmt.Sprintf("<a href=\"/%s\">%s</a>", strings.Join(linkWords, "/"), link)
// Insert the link into the paragraph, replacing the original words
paragraph = strings.Replace(paragraph, strings.Join(linkWords, " "), link, 1)
}
return paragraph
}
func thanksForYourPatience() (string, error) {
// check size of fileFolder
averageFileAge, size, err := waitAMinuteMan(fileFolder)
if err != nil {
return fmt.Sprintf("Error calculating size of file folder %s: %v\n", fileFolder, err), err
}
if size > 10*1024*1024 { // If the size of the folder is greater than 100 MB
// If the file is older than the average file age, delete it
err = filepath.Walk(fileFolder, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
if info.ModTime().Unix() < time.Now().Unix()-averageFileAge {
fmt.Printf("Deleting old file %s\n", info.Name())
if err := os.Remove(info.Name()); err != nil {
fmt.Printf("Error deleting file %s: %v\n", info.Name(), err)
return err
}
}
}
return err
})
}
if err != nil {
return fmt.Sprintf("Error walking through file folder %s: %v\n", fileFolder, err), err
}
sizeInKB := size / (1024) // Convert size to kilobytes
return fmt.Sprintf("Average file age: %d seconds, Total size: %dKB", averageFileAge, sizeInKB), nil
}
type PocketGraph map[string][]string
type Words []string
func isShePervertedLikeMe() PocketGraph {
var ConnectionWords = []string{
"and", "or", "but", "so", "because",
"however", "while", "in", "on", "at", "to", "for",
"with", "without", "about", "against", "between",
"the", "a", "an", "this", "that",
"his", "her", "their", "our", "my",
"your", "its", "whose", "which", "who",
"what", "when", "where", "why", "how",
"whoever", "whatever", "whenever",
}
// read words.txt into a list
wordsBytes, err := os.ReadFile("word_lists/jaggedpill.txt")
if err != nil {
fmt.Println("Error reading words.txt:", err)
}
words := strings.Split(string(wordsBytes), "\n")
tree := make(PocketGraph)
for _, word := range ConnectionWords {
var percentageOfWords float64 = 0.005
var numberOfConnections = float64(len(words)) * percentageOfWords
for j := 0; j < int(numberOfConnections); j++ {
randomWord := words[rand.Intn(len(words))]
if randomWord == "" {
continue // skip empty lines
}
tree[word] = append(tree[word], randomWord)
}
}
// take each word and link it to a random word
for _, word := range words {
// generate a random word from the list
if word == "" {
continue // skip empty lines
}
numberOfLinks := rand.Intn(100) + 1
for i := 0; i < numberOfLinks; i++ {
randomWord := words[rand.Intn(len(words))]
tree[word] = []string{randomWord}
}
// connect word to connector words?
shouldConnect := rand.Intn(2) == 0
if shouldConnect {
numConnectionWords := rand.Intn(len(ConnectionWords)) + 1 // Random number of connection words between 1 and 3
for j := 0; j < numConnectionWords; j++ {
connectionWord := ConnectionWords[rand.Intn(len(ConnectionWords))]
tree[word] = append(tree[word], connectionWord)
}
}
}
return tree
}
// forgiveMeLove generates N random words from the tree
func (m PocketGraph) forgiveMeLove(n int) Words {
if n <= 0 {
return nil
}
// Create a slice to hold the random words
randomWords := make(Words, 0, n)
// Generate N random words from the tree
word := m[iWentToYourHouse(m)] // Start with a random word from the tree
for i := 0; i < n-1; i++ {
if len(word) == 0 {
break
}
random := rand.Intn(len(word))
nextWord := word[random] // Randomly select a word from the current word's linked words
randomWords = append(randomWords, nextWord)
word = m[nextWord] // Update the word to the next linked word
}
return randomWords
}
func (w Words) string() string {
if len(w) == 0 {
return ""
}
return strings.Join(w, " ")
}
func iWentToYourHouse(m PocketGraph) string {
r := rand.Intn(len(m))
for k := range m {
if r == 0 {
return k
}
r--
}
panic("unreachable")
}
func waitAMinuteMan(path string) (int64, int64, error) {
var size int64
var fileAges []int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Add the age of the file in seconds to the medianFileAge slice
fileAges = append(fileAges, time.Now().Unix()-info.ModTime().Unix())
// Add the size of the file to the total size
size += info.Size()
}
return err
})
averageFileAge := int64(0)
if len(fileAges) > 0 {
for _, age := range fileAges {
averageFileAge += age
}
averageFileAge /= int64(len(fileAges))
} else {
averageFileAge = 0 // No files, so average age is 0
}
return averageFileAge, size, err
}