280 lines
6.2 KiB
Go
280 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
tuser "git.tilde.town/tildetown/town/user"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
const (
|
|
signupDir = "/town/signups"
|
|
acceptedDir = "/town/signups/accepted"
|
|
rejectedDir = "/town/signups/rejected"
|
|
)
|
|
|
|
func getTitle() string {
|
|
titles := []string{
|
|
"yo bum rush the show",
|
|
"can i kick it?",
|
|
"super nintendo sega genesis",
|
|
"birthdays was the worst days",
|
|
"where were you when we were getting high?",
|
|
"it's real time, real time, time to get real",
|
|
}
|
|
return titles[rand.Intn(len(titles))]
|
|
}
|
|
|
|
type townSignup struct {
|
|
When time.Time
|
|
DecisionTime time.Time
|
|
Decision string
|
|
Filename string
|
|
Answers map[string]string
|
|
}
|
|
|
|
func (s townSignup) Accept() error {
|
|
return s.review("accept")
|
|
}
|
|
|
|
func (s townSignup) Reject() error {
|
|
return s.review("reject")
|
|
}
|
|
|
|
func (s townSignup) review(decision string) error {
|
|
s.DecisionTime = time.Now()
|
|
s.Decision = "decision"
|
|
oldpath := path.Join(signupDir, s.Filename)
|
|
newpath := path.Join(acceptedDir, s.Filename)
|
|
if decision == "reject" {
|
|
newpath = path.Join(rejectedDir, s.Filename)
|
|
}
|
|
return os.Rename(oldpath, newpath)
|
|
}
|
|
|
|
func (s townSignup) Render() string {
|
|
out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.When.Format("2006-01-02 15:04"))
|
|
|
|
for k, v := range s.Answers {
|
|
out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k)
|
|
out += strings.TrimSpace(v)
|
|
out += "\n\n"
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func getSignups() ([]townSignup, error) {
|
|
entries, err := os.ReadDir(signupDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read '%s': %w", signupDir, err)
|
|
}
|
|
out := []townSignup{}
|
|
for _, entry := range entries {
|
|
if !strings.HasSuffix(entry.Name(), "json") {
|
|
continue
|
|
}
|
|
|
|
abs := path.Join(signupDir, entry.Name())
|
|
data, err := os.ReadFile(abs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not read signup file '%s': %w", abs, err)
|
|
}
|
|
|
|
fmt.Println(string(data))
|
|
|
|
var signup townSignup
|
|
err = json.Unmarshal(data, &signup)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal signup file '%s': %w", abs, err)
|
|
}
|
|
signup.Filename = entry.Name()
|
|
|
|
out = append(out, signup)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func _main() error {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("that's my purse. I don't know you! %w", err)
|
|
}
|
|
isAdmin, err := tuser.IsAdmin(u)
|
|
if err != nil {
|
|
return fmt.Errorf("that's my purse. I don't know you! %w", err)
|
|
}
|
|
|
|
if !isAdmin {
|
|
return errors.New("this command can only be run by a town admin")
|
|
}
|
|
rand.Seed(time.Now().Unix())
|
|
|
|
signups, err := getSignups()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get signups: %w", err)
|
|
}
|
|
|
|
signupIx := 0
|
|
|
|
title := tview.NewTextView()
|
|
title.SetText(getTitle())
|
|
title.SetTextAlign(tview.AlignCenter)
|
|
title.SetTextColor(tcell.ColorPurple)
|
|
title.SetBackgroundColor(tcell.ColorBlack)
|
|
|
|
appView := tview.NewTextView()
|
|
appView.SetDynamicColors(true)
|
|
if len(signups) == 0 {
|
|
appView.SetText("no signups found.")
|
|
} else {
|
|
appView.SetText(signups[signupIx].Render())
|
|
}
|
|
|
|
legend := tview.NewTextView()
|
|
legend.SetText("s: skip r: random A: approve R: reject N: notate Q: quit")
|
|
legend.SetTextColor(tcell.ColorPurple)
|
|
legend.SetTextAlign(tview.AlignCenter)
|
|
legend.SetBackgroundColor(tcell.ColorBlack)
|
|
|
|
count := tview.NewTextView()
|
|
count.SetDynamicColors(true)
|
|
updateCount := func() {
|
|
count.SetText(fmt.Sprintf("[-:-:b]%d pending signups[-:-:-]", len(signups)))
|
|
}
|
|
updateCount()
|
|
|
|
bottomFlex := tview.NewFlex()
|
|
bottomFlex.SetDirection(tview.FlexColumn)
|
|
bottomFlex.AddItem(count, 0, 1, false)
|
|
bottomFlex.AddItem(legend, 0, 10, false)
|
|
|
|
mainFlex := tview.NewFlex()
|
|
mainFlex.SetDirection(tview.FlexRow)
|
|
mainFlex.AddItem(title, 1, -1, false)
|
|
mainFlex.AddItem(appView, 0, 1, true)
|
|
mainFlex.AddItem(bottomFlex, 1, -1, false)
|
|
|
|
pages := tview.NewPages()
|
|
|
|
errorModal := tview.NewModal()
|
|
errorModal.AddButtons([]string{"damn"})
|
|
errorModal.SetDoneFunc(func(ix int, _ string) {
|
|
pages.SwitchToPage("main")
|
|
})
|
|
|
|
notate := tview.NewForm()
|
|
notate.AddTextArea("note", "", 80, 10, 1000, func(string) {})
|
|
notate.AddButton("submit", func() {
|
|
// TODO add note; this will require re-serializing and saving the file
|
|
pages.SwitchToPage("main")
|
|
})
|
|
notate.AddButton("cancel", func() {
|
|
pages.SwitchToPage("main")
|
|
})
|
|
|
|
pages.AddPage("main", mainFlex, true, true)
|
|
pages.AddPage("error", errorModal, false, false)
|
|
pages.AddPage("notate", notate, true, false)
|
|
|
|
app := tview.NewApplication()
|
|
app.SetRoot(pages, true)
|
|
|
|
// TODO replace imperative shit with a signupManager
|
|
advanceSignup := func() {
|
|
if len(signups) == 0 {
|
|
appView.SetText("no signups found.")
|
|
return
|
|
}
|
|
signupIx++
|
|
if signupIx == len(signups) {
|
|
signupIx = 0
|
|
}
|
|
appView.SetText(signups[signupIx].Render())
|
|
}
|
|
|
|
removeSignup := func(signup townSignup) {
|
|
newSignups := []townSignup{}
|
|
for ix, s := range signups {
|
|
if ix != signupIx {
|
|
newSignups = append(newSignups, s)
|
|
}
|
|
}
|
|
signups = newSignups
|
|
if len(signups) > 0 {
|
|
if signupIx >= len(signups) {
|
|
signupIx = 0
|
|
}
|
|
}
|
|
appView.SetText(signups[signupIx].Render())
|
|
}
|
|
|
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
|
switch event.Rune() {
|
|
case 's':
|
|
advanceSignup()
|
|
case 'r':
|
|
if len(signups) > 0 {
|
|
signupIx = rand.Intn(len(signups))
|
|
appView.SetText(signups[signupIx].Render())
|
|
}
|
|
case 'A':
|
|
if len(signups) == 0 {
|
|
return nil
|
|
}
|
|
signup := signups[signupIx]
|
|
err := signup.Accept()
|
|
if err != nil {
|
|
errorModal.SetText(fmt.Sprintf("error! failed to approve '%s': %s", signup.Filename, err.Error()))
|
|
pages.SwitchToPage("error")
|
|
return nil
|
|
}
|
|
removeSignup(signup)
|
|
updateCount()
|
|
case 'R':
|
|
if len(signups) == 0 {
|
|
return nil
|
|
}
|
|
signup := signups[signupIx]
|
|
err = signup.Reject()
|
|
if err != nil {
|
|
errorModal.SetText(fmt.Sprintf("error! failed to reject '%s': %s", signup.Filename, err.Error()))
|
|
pages.SwitchToPage("error")
|
|
return nil
|
|
}
|
|
removeSignup(signup)
|
|
updateCount()
|
|
case 'N':
|
|
if len(signups) == 0 {
|
|
return nil
|
|
}
|
|
pages.SwitchToPage("notate")
|
|
return nil
|
|
case 'Q':
|
|
app.Stop()
|
|
}
|
|
|
|
return event
|
|
})
|
|
|
|
return app.Run()
|
|
}
|
|
|
|
func main() {
|
|
err := _main()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|