neofeels/ttbp/ttbp.go

330 lines
7.8 KiB
Go

package ttbp
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os"
"os/user"
"path"
"sort"
"strings"
"time"
"git.tilde.town/nbsp/neofeels/config"
"github.com/yuin/goldmark"
)
var (
PathVar = "/var/global/ttbp"
PathVarWWW = path.Join(PathVar, "www")
PathLive = "https://tilde.town/~"
PathUserFile = path.Join(PathVar, "users.txt")
PathGraff = path.Join(PathVar, "graffiti")
PathWall = path.Join(PathGraff, "wall.txt")
PathWallLock = path.Join(PathGraff, ".lock")
PathUser = os.Getenv("HOME")
PathUserFeels = path.Join(PathUser, ".ttbp")
PathUserHTML = path.Join(PathUser, "public_html")
PathUserConfig = path.Join(PathUserFeels, "config")
PathUserEntries = path.Join(PathUserFeels, "entries")
PathUserWWW = path.Join(PathUserFeels, "www")
PathUserRc = path.Join(PathUserConfig, "ttbprc")
PathUserNopub = path.Join(PathUserConfig, "nopub")
PathUserSubs = path.Join(PathUserConfig, "subs")
)
type User struct {
Name string
Publishing bool
PublishDir string
LastPublished time.Time
}
// GetUsers gets all users with a valid ttbp config, with their name, publishing
// settings, and date of last publish, if any.
func GetUsers() (users []User) {
userDirs, _ := os.ReadDir("/home")
for _, user := range userDirs {
if !user.IsDir() {
continue
}
if file, err := os.Open(path.Join("/home", user.Name(), ".ttbp/config/ttbprc")); err == nil {
defer file.Close()
// get user config
var config config.Config
decoder := json.NewDecoder(file)
err = decoder.Decode(&config)
if err != nil {
continue
}
// get last published file
entries, err := os.ReadDir(path.Join("/home", user.Name(), ".ttbp/entries"))
if err != nil {
continue
}
var lastPublished time.Time = *new(time.Time)
if len(entries) > 0 {
info, err := entries[len(entries)-1].Info()
if err != nil {
continue
}
lastPublished = info.ModTime()
}
users = append(users, User{
Name: user.Name(),
Publishing: config.Publishing,
PublishDir: config.PublishDir,
LastPublished: lastPublished,
})
}
}
return users
}
// SortUsersByRecent sorts users, putting the users with the most recent
// published feels at the beginning.
func SortUsersByRecent(users []User) []User {
sort.Slice(users, func(i, j int) bool {
return users[i].LastPublished.After(users[j].LastPublished)
})
return users
}
type Post struct {
Date time.Time
LastEdited time.Time
Words int
Author string
Nopub bool
}
func GetPostsForUser(user string) (posts []Post) {
postFiles, err := os.ReadDir(path.Join("/home", user, ".ttbp/entries"))
if err != nil {
return
}
nopubFile, err := os.OpenFile(PathUserNopub, os.O_RDONLY|os.O_CREATE, 0644)
if err != nil {
return
}
defer nopubFile.Close()
var nopubs []string
nopubScanner := bufio.NewScanner(nopubFile)
for nopubScanner.Scan() {
nopubs = append(nopubs, nopubScanner.Text())
}
for _, post := range postFiles {
// retrieve date of file
// assume file ends in .txt
fileDate, err := time.Parse("20060102.txt", post.Name())
if err != nil {
continue
}
if file, err := os.Open(path.Join("/home", user, ".ttbp/entries", post.Name())); err == nil {
defer file.Close()
// get number of words in file
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
count := 0
for scanner.Scan() {
count++
}
// get modtime of file
stat, err := file.Stat()
if err != nil {
continue
}
// see if file is in nopub
nopub := false
for _, name := range nopubs {
if name == post.Name() {
nopub = true
break
}
}
posts = append([]Post{Post{
Author: user,
Date: fileDate,
LastEdited: stat.ModTime(),
Words: count,
Nopub: nopub,
}}, posts...)
}
}
return
}
// SortUsersByRecent sorts posts, putting the posts that were most recently
// edited first in the list.
func SortPostsByRecent(posts []Post) []Post {
sort.Slice(posts, func(i, j int) bool {
return posts[i].LastEdited.After(posts[j].LastEdited)
})
return posts
}
type Subscriptions struct {
users []string
}
func GetSubscriptions() *Subscriptions {
file, err := os.OpenFile(PathUserSubs, os.O_RDONLY|os.O_CREATE, 0600)
if err != nil {
return &Subscriptions{}
}
defer file.Close()
var users []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
users = append(users, scanner.Text())
}
return &Subscriptions{
users: users,
}
}
func (subscriptions *Subscriptions) IsSubscribed(user User) bool {
for _, sub := range subscriptions.users {
if sub == user.Name {
return true
}
}
return false
}
func (subscriptions *Subscriptions) Subscribe(user User) {
subscriptions.users = append(subscriptions.users, user.Name)
}
func (subscriptions *Subscriptions) Unsubscribe(user User) {
for i, sub := range subscriptions.users {
if sub == user.Name {
subscriptions.users = append(subscriptions.users[:i], subscriptions.users[i+1:]...)
return
}
}
}
func (subscriptions *Subscriptions) Write() {
file, err := os.Create(PathUserSubs)
if err != nil {
return
}
defer file.Close()
writer := bufio.NewWriter(file)
for _, line := range subscriptions.users {
_, err := writer.WriteString(line + "\n")
if err != nil {
return
}
}
writer.Flush()
}
func NewNopub(t time.Time) {
cfg, err := config.Read()
if err != nil || (!cfg.Nopub && cfg.Publishing) {
return
}
dateString := t.Format("20060102.txt")
file, err := os.OpenFile(PathUserNopub, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if scanner.Text() == dateString {
return
}
}
writer := bufio.NewWriter(file)
writer.WriteString(dateString)
writer.Flush()
}
func Publish(t time.Time) {
cfg, err := config.Read()
if err != nil {
return // TODO: expose this error to the user
}
if cfg.Publishing {
file, err := os.Create(path.Join(PathUserWWW, "index.html"))
defer file.Close()
// load header and footer
header, err := os.ReadFile(path.Join(PathUserConfig, "header.txt"))
footer, err := os.ReadFile(path.Join(PathUserConfig, "footer.txt"))
if err != nil {
return
}
writer := bufio.NewWriter(file)
writer.WriteString(fmt.Sprintf("<!-- generated by neofeels on %s — https://tilde.town/~nbsp/neofeels -->\n", time.Now().Format(time.DateTime)))
writer.WriteString(string(header) + "\n")
user, _ := user.Current()
for _, post := range GetPostsForUser(user.Username) {
if !post.Nopub && post.Words > 0 {
writePage(post, header, footer)
writer.WriteString(writeEntry(post) + "\n")
}
}
writer.WriteString(string(footer))
writer.Flush()
}
// TODO: gopher
}
func writePage(post Post, header, footer []byte) {
dateString := post.Date.Format("20060102.html")
file, err := os.Create(path.Join(PathUserWWW, dateString))
if err != nil {
return
}
writer := bufio.NewWriter(file)
writer.WriteString(fmt.Sprintf("<-- generated by neofeels on %s — https://tilde.town/~nbsp/neofeels -->\n", time.Now().Format(time.DateTime)))
writer.WriteString(string(header) + "\n")
writer.WriteString(writeEntry(post) + "\n")
writer.WriteString(string(footer))
writer.Flush()
}
func writeEntry(post Post) string {
dateString := post.Date.Format("20060102")
file, err := os.ReadFile(path.Join(PathUserEntries, dateString+".txt"))
if err != nil {
return ""
}
var buf bytes.Buffer
goldmark.Convert(file, &buf)
return fmt.Sprintf(
` <p><a name="%s"></a><br /><br /></p>
<div class="entry">
<h5><a href="#%s">%d</a> %s %d</h5>
%s
<p class="permalink"><a href="%s.html">permalink</a></p>
</div>
`,
dateString, dateString, post.Date.Day(),
strings.ToLower(post.Date.Month().String()), post.Date.Year(),
buf.String(), dateString,
)
}