2025-01-06 21:53:29 +00:00
|
|
|
package ttbp
|
|
|
|
|
|
|
|
import (
|
2025-01-07 00:16:11 +00:00
|
|
|
"bufio"
|
2025-01-07 20:32:19 +00:00
|
|
|
"bytes"
|
2025-01-06 21:53:29 +00:00
|
|
|
"encoding/json"
|
2025-01-07 20:32:19 +00:00
|
|
|
"fmt"
|
2025-01-06 21:53:29 +00:00
|
|
|
"os"
|
2025-01-07 20:32:19 +00:00
|
|
|
"os/user"
|
2025-01-06 21:53:29 +00:00
|
|
|
"path"
|
|
|
|
"sort"
|
2025-01-07 20:32:19 +00:00
|
|
|
"strings"
|
2025-01-06 21:53:29 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.tilde.town/nbsp/neofeels/config"
|
2025-01-07 20:32:19 +00:00
|
|
|
"github.com/yuin/goldmark"
|
2025-01-06 21:53:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2025-01-08 00:08:50 +00:00
|
|
|
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")
|
2025-01-08 02:06:14 +00:00
|
|
|
PathUserBuried = path.Join(PathUserFeels, "buried")
|
2025-01-08 02:21:56 +00:00
|
|
|
PathUserBackups = path.Join(PathUserFeels, "backups")
|
2025-01-08 00:08:50 +00:00
|
|
|
PathUserWWW = path.Join(PathUserFeels, "www")
|
|
|
|
PathUserRc = path.Join(PathUserConfig, "ttbprc")
|
|
|
|
PathUserNopub = path.Join(PathUserConfig, "nopub")
|
|
|
|
PathUserSubs = path.Join(PathUserConfig, "subs")
|
2025-01-06 21:53:29 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2025-01-07 00:16:11 +00:00
|
|
|
|
|
|
|
type Post struct {
|
2025-01-07 00:55:07 +00:00
|
|
|
Date time.Time
|
|
|
|
LastEdited time.Time
|
|
|
|
Words int
|
|
|
|
Author string
|
2025-01-07 20:32:19 +00:00
|
|
|
Nopub bool
|
2025-01-07 00:16:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func GetPostsForUser(user string) (posts []Post) {
|
|
|
|
postFiles, err := os.ReadDir(path.Join("/home", user, ".ttbp/entries"))
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
2025-01-08 00:08:50 +00:00
|
|
|
nopubFile, err := os.OpenFile(PathUserNopub, os.O_RDONLY|os.O_CREATE, 0644)
|
2025-01-07 20:32:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer nopubFile.Close()
|
|
|
|
var nopubs []string
|
|
|
|
nopubScanner := bufio.NewScanner(nopubFile)
|
|
|
|
for nopubScanner.Scan() {
|
|
|
|
nopubs = append(nopubs, nopubScanner.Text())
|
|
|
|
}
|
2025-01-07 00:16:11 +00:00
|
|
|
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()
|
2025-01-07 00:55:07 +00:00
|
|
|
|
|
|
|
// get number of words in file
|
2025-01-07 00:16:11 +00:00
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
scanner.Split(bufio.ScanWords)
|
2025-01-07 00:55:07 +00:00
|
|
|
count := 0
|
2025-01-07 00:16:11 +00:00
|
|
|
for scanner.Scan() {
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
|
2025-01-07 00:55:07 +00:00
|
|
|
// get modtime of file
|
|
|
|
stat, err := file.Stat()
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-01-07 20:32:19 +00:00
|
|
|
// see if file is in nopub
|
|
|
|
nopub := false
|
|
|
|
for _, name := range nopubs {
|
|
|
|
if name == post.Name() {
|
|
|
|
nopub = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-07 00:55:07 +00:00
|
|
|
posts = append([]Post{Post{
|
|
|
|
Author: user,
|
|
|
|
Date: fileDate,
|
|
|
|
LastEdited: stat.ModTime(),
|
|
|
|
Words: count,
|
2025-01-07 20:32:19 +00:00
|
|
|
Nopub: nopub,
|
2025-01-07 00:55:07 +00:00
|
|
|
}}, posts...)
|
|
|
|
}
|
2025-01-07 00:16:11 +00:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2025-01-07 00:55:07 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2025-01-07 11:50:02 +00:00
|
|
|
|
|
|
|
type Subscriptions struct {
|
2025-01-07 12:13:03 +00:00
|
|
|
users []string
|
2025-01-07 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func GetSubscriptions() *Subscriptions {
|
2025-01-08 00:08:50 +00:00
|
|
|
file, err := os.OpenFile(PathUserSubs, os.O_RDONLY|os.O_CREATE, 0600)
|
2025-01-07 12:13:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return &Subscriptions{}
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
var users []string
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
|
|
users = append(users, scanner.Text())
|
|
|
|
}
|
|
|
|
|
2025-01-07 11:50:02 +00:00
|
|
|
return &Subscriptions{
|
2025-01-07 12:13:03 +00:00
|
|
|
users: users,
|
2025-01-07 11:50:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (subscriptions *Subscriptions) IsSubscribed(user User) bool {
|
|
|
|
for _, sub := range subscriptions.users {
|
2025-01-07 12:13:03 +00:00
|
|
|
if sub == user.Name {
|
2025-01-07 11:50:02 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (subscriptions *Subscriptions) Subscribe(user User) {
|
2025-01-07 12:13:03 +00:00
|
|
|
subscriptions.users = append(subscriptions.users, user.Name)
|
2025-01-07 11:50:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (subscriptions *Subscriptions) Unsubscribe(user User) {
|
|
|
|
for i, sub := range subscriptions.users {
|
2025-01-07 12:13:03 +00:00
|
|
|
if sub == user.Name {
|
2025-01-07 11:50:02 +00:00
|
|
|
subscriptions.users = append(subscriptions.users[:i], subscriptions.users[i+1:]...)
|
2025-01-07 12:13:03 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-07 12:25:35 +00:00
|
|
|
func (subscriptions *Subscriptions) Write() {
|
2025-01-08 00:08:50 +00:00
|
|
|
file, err := os.Create(PathUserSubs)
|
2025-01-07 12:13:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
writer := bufio.NewWriter(file)
|
|
|
|
for _, line := range subscriptions.users {
|
|
|
|
_, err := writer.WriteString(line + "\n")
|
|
|
|
if err != nil {
|
2025-01-07 11:50:02 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2025-01-07 12:13:03 +00:00
|
|
|
|
|
|
|
writer.Flush()
|
2025-01-07 11:50:02 +00:00
|
|
|
}
|
2025-01-07 20:32:19 +00:00
|
|
|
|
|
|
|
func NewNopub(t time.Time) {
|
|
|
|
cfg, err := config.Read()
|
|
|
|
if err != nil || (!cfg.Nopub && cfg.Publishing) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dateString := t.Format("20060102.txt")
|
2025-01-08 00:08:50 +00:00
|
|
|
file, err := os.OpenFile(PathUserNopub, os.O_RDWR|os.O_CREATE, 0600)
|
2025-01-07 20:32:19 +00:00
|
|
|
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 {
|
2025-01-08 00:45:01 +00:00
|
|
|
if _, err := os.Stat(PathUserWWW); os.IsNotExist(err) {
|
|
|
|
os.MkdirAll(PathUserWWW, 0700)
|
|
|
|
os.Symlink(path.Join(PathUserConfig, "style.css"), path.Join(PathUserWWW, "style.css"))
|
|
|
|
}
|
2025-01-08 00:08:50 +00:00
|
|
|
file, err := os.Create(path.Join(PathUserWWW, "index.html"))
|
2025-01-07 20:32:19 +00:00
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
// load header and footer
|
2025-01-08 00:08:50 +00:00
|
|
|
header, err := os.ReadFile(path.Join(PathUserConfig, "header.txt"))
|
|
|
|
footer, err := os.ReadFile(path.Join(PathUserConfig, "footer.txt"))
|
2025-01-07 20:32:19 +00:00
|
|
|
|
|
|
|
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) {
|
2025-01-08 00:08:50 +00:00
|
|
|
if !post.Nopub && post.Words > 0 {
|
2025-01-07 20:32:19 +00:00
|
|
|
writePage(post, header, footer)
|
|
|
|
writer.WriteString(writeEntry(post) + "\n")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
writer.WriteString(string(footer))
|
|
|
|
writer.Flush()
|
|
|
|
}
|
|
|
|
// TODO: gopher
|
|
|
|
}
|
|
|
|
|
2025-01-08 01:43:27 +00:00
|
|
|
func Unpublish() {
|
|
|
|
cfg, err := config.Read()
|
|
|
|
if err != nil {
|
|
|
|
return // TODO: expose
|
|
|
|
}
|
|
|
|
if cfg.Publishing {
|
|
|
|
os.RemoveAll(PathUserWWW)
|
|
|
|
os.RemoveAll(path.Join(PathUserHTML, cfg.PublishDir))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-07 20:32:19 +00:00
|
|
|
func writePage(post Post, header, footer []byte) {
|
2025-01-08 00:08:50 +00:00
|
|
|
dateString := post.Date.Format("20060102.html")
|
|
|
|
file, err := os.Create(path.Join(PathUserWWW, dateString))
|
2025-01-07 20:32:19 +00:00
|
|
|
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")
|
2025-01-08 00:08:50 +00:00
|
|
|
file, err := os.ReadFile(path.Join(PathUserEntries, dateString+".txt"))
|
2025-01-07 20:32:19 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
}
|
2025-01-08 01:14:15 +00:00
|
|
|
|
|
|
|
func GraffitiFree() bool {
|
|
|
|
_, err := os.Stat(PathWallLock)
|
|
|
|
return os.IsNotExist(err)
|
|
|
|
}
|