town/cmd/review/main.go

264 lines
6.0 KiB
Go

package main
import (
"database/sql"
"errors"
"fmt"
"math/rand"
"os"
"os/user"
"strings"
"time"
"git.tilde.town/tildetown/town/models"
//"git.tilde.town/tildetown/town/review"
"git.tilde.town/tildetown/town/signup"
tuser "git.tilde.town/tildetown/town/user"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
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))]
}
// TODO affordance for cleaning up email response
type reviewer struct {
db *sql.DB
adminName string
}
func newReviewer(db *sql.DB, adminName string) *reviewer {
return &reviewer{db: db, adminName: adminName}
}
func (r *reviewer) Review(s *models.TownSignup, decision models.SignupDecision) error {
s.DecisionTime = time.Now()
s.Decision = decision
s.DecidedBy = r.adminName
s.Insert(r.db)
return nil
}
func renderSignup(s models.TownSignup) string {
out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.Created.Format("2006-01-02 15:04"))
pairs := map[string]string{
"e-mail": s.Email,
"how found / referral": s.How,
"why like town / what do": s.Why,
"links": s.Links,
}
for k, v := range pairs {
out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k)
out += strings.TrimSpace(v)
out += "\n\n"
}
return out
}
func _main() error {
/*
TODO will use this for invites
userDB, err := review.ConnectDB()
if err != nil {
return fmt.Errorf("could not connect to user database: %w", err)
}
*/
signupDB, err := signup.ConnectDB()
if err != nil {
return fmt.Errorf("could not connect to signups database: %w", err)
}
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")
}
r := newReviewer(signupDB, u.Username)
rand.Seed(time.Now().Unix())
su := models.TownSignup{}
signups, err := su.All(signupDB)
if err != nil {
return fmt.Errorf("could not fetch 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(renderSignup(*signups[signupIx]))
}
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() {
// add note and update
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(renderSignup(*signups[signupIx]))
}
removeSignup := func(signup *models.TownSignup) {
newSignups := []*models.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(renderSignup(*signups[signupIx]))
}
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(renderSignup(*signups[signupIx]))
}
case 'A':
if len(signups) == 0 {
return nil
}
// TODO modal for collecting clean email
signup := signups[signupIx]
err := r.Review(signup, models.SignupAccepted)
if err != nil {
errorModal.SetText(fmt.Sprintf("error! failed to approve '%d': %s", signup.ID, err.Error()))
pages.SwitchToPage("error")
return nil
}
removeSignup(signup)
updateCount()
// TODO generate invite token
// TODO send invite email
case 'R':
if len(signups) == 0 {
return nil
}
signup := signups[signupIx]
err = r.Review(signup, models.SignupRejected)
if err != nil {
errorModal.SetText(fmt.Sprintf("error! failed to reject '%d': %s", signup.ID, 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)
}
}