diff --git a/cmd/signup/main.go b/cmd/signup/main.go index 71f74ca..c57f966 100644 --- a/cmd/signup/main.go +++ b/cmd/signup/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "fmt" "log" "os" @@ -8,29 +9,25 @@ import ( "strings" "time" - "encoding/json" - + "git.tilde.town/tildetown/town/signup" "github.com/MakeNowJust/heredoc/v2" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) +// TODO remove sidebar +// TODO add /help + const ( - maxInputLength = 10000 - signupDirectory = "/town/signups" - logDir = "/town/var/signup" + maxInputLength = 10000 + logDir = "/town/var/signup" ) type scene struct { Name string Description string - Key string Host *character -} - -type townApplication struct { - When time.Time - Answers map[string]string + Write func([]byte) } type character struct { @@ -65,14 +62,21 @@ func main() { } logger := log.New(logF, "", log.Ldate|log.Ltime) - err = _main(logger) + + db, err := signup.NewDB() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(2) } + + err = _main(logger, db) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(3) + } } -func _main(l *log.Logger) error { +func _main(l *log.Logger, db *signup.DB) error { l.Println("starting a session") pages := tview.NewPages() mainFlex := tview.NewFlex() @@ -126,31 +130,15 @@ func _main(l *log.Logger) error { player := newCharacter("you", "TODO") - townApp := &townApplication{ - Answers: map[string]string{}, - } + su := &signup.TownSignup{ID: -1} save := func() { - townApp.When = time.Now() - output, err := json.Marshal(townApp) - if err != nil { - l.Printf("could not serialize stuff: %s", err.Error()) - l.Printf("dumping values: %v", townApp) - return - } + su.Created = time.Now() + err := db.InsertSignup(su) - f, err := os.Create(path.Join(signupDirectory, fmt.Sprintf("%d.json", townApp.When.Unix()))) if err != nil { - l.Printf("could not open signup file: %s", err.Error()) - l.Printf("dumping values: %s", string(output)) - return - } - defer f.Close() - - _, err = f.Write(output) - if err != nil { - l.Printf("failed to write to file: %s", err.Error()) - l.Printf("dumping values: %s", string(output)) + l.Printf("failed to write to db: %s", err.Error()) + l.Printf("dumping values: %#v", su) return } } @@ -176,7 +164,9 @@ func _main(l *log.Logger) error { when you're ready to move on, [-:-:b]/nod[-:-:-] `), Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"), - Key: "email", + Write: func(b []byte) { + su.Email = string(b) + }, }, { Name: "nodded", @@ -198,7 +188,9 @@ func _main(l *log.Logger) error { just say your answer out loud. when you've said what you want, [-:-:b]/lean[-:-:-] against a tree. `), - Key: "how", + Write: func(b []byte) { + su.How = string(b) + }, Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."), }, { @@ -217,7 +209,9 @@ func _main(l *log.Logger) error { as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-] around in this void. `), - Key: "why", + Write: func(b []byte) { + su.Why = string(b) + }, Host: newCharacter("the vcr", "a black and grey VCR from 1991"), }, { @@ -241,7 +235,9 @@ func _main(l *log.Logger) error { when you're happy you can submit this whole experience by leaving the store. just [-:-:b]/open[-:-:-] the door. `), - Key: "where", + Write: func(b []byte) { + su.Links = string(b) + }, Host: newCharacter("the mop", "a greying mop with a wooden handle."), }, { @@ -255,21 +251,29 @@ func _main(l *log.Logger) error { ok bye have a good one~ `), - Key: "extra", + Write: func(b []byte) { + su.Extra = string(b) + }, }, } sceneIx := 0 currentScene := scenes[sceneIx] + inputBuff := []byte{} + inputWriter := bytes.NewBuffer(inputBuff) + advanceScene := func(fromScene, sorryMsg string) { if currentScene.Name != fromScene { return } - if len(townApp.Answers[currentScene.Key]) == 0 { + if inputWriter.Len() == 0 { fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg)) return } + currentScene.Write(inputBuff) + inputBuff = []byte{} + inputWriter = bytes.NewBuffer(inputBuff) sceneIx++ currentScene = scenes[sceneIx] fmt.Fprintln(msgScroll, heredoc.Doc(` @@ -310,15 +314,22 @@ func _main(l *log.Logger) error { } return } - if len(townApp.Answers[currentScene.Key]) > maxInputLength { + if inputWriter.Len() > maxInputLength { fmt.Fprintln(msgScroll, currentScene.Host.Say("sorry I've heard more than I can remember :( maybe it's time to move on")) return } fmt.Fprintln(msgScroll, player.Say(msg)) - townApp.Answers[currentScene.Key] += ("\n" + msg) + fmt.Fprintln(inputWriter, msg) } + defer func() { + if currentScene.Name == "done" { + currentScene.Write(inputBuff) + db.UpdateSignup(su) + } + }() + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlD: diff --git a/signup/signup.go b/signup/signup.go new file mode 100644 index 0000000..9b0c1f8 --- /dev/null +++ b/signup/signup.go @@ -0,0 +1,108 @@ +package signup + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" + "time" +) + +type AdminNote struct { + ID int64 + Admin string + Note string + When time.Time +} + +type SignupDecision string +type UserState string + +const ( + SignupAccepted = "accepted" + SignupRejected = "rejected" + StateActive = "active" + StateTempBan = "temp_banned" + StateBan = "banned" +) + +type TownSignup struct { + ID int64 + Created time.Time + Email string + How string + Why string + Links string + Extra string + Notes []AdminNote + Decision SignupDecision +} + +type TownAccount struct { + ID int + Emails []string + Username string + Signup int + Notes []AdminNote + State UserState + Admin bool +} + +type DB struct { + db *sql.DB +} + +func NewDB() (*DB, error) { + db, err := sql.Open("sqlite3", "/town/var/signups.db") + if err != nil { + return nil, err + } + + return &DB{ + db: db, + }, nil +} + +func (d *DB) InsertSignup(su *TownSignup) error { + stmt, err := d.db.Prepare(` + INSERT INTO signups (created, email, how, why, links, extra) VALUES( + ?, ?, ?, ?, ?, ? + ) RETURNING id + `) + if err != nil { + return err + } + + result, err := stmt.Exec(su.Created.Unix(), su.Email, su.How, su.Why, su.Links, su.Extra) + if err != nil { + return err + } + + liid, err := result.LastInsertId() + if err != nil { + return err + } + + su.ID = liid + + return nil +} + +func (d *DB) UpdateSignup(su *TownSignup) error { + if su.ID < 0 { + return nil + } + stmt, err := d.db.Prepare(` + UPDATE signups (email, how, why, links, extra) VALUES( + ?, ?, ?, ?, ? + ) + `) + if err != nil { + return err + } + + _, err = stmt.Exec(su.Email, su.How, su.Why, su.Links, su.Extra) + if err != nil { + return err + } + + return nil +} diff --git a/sql/create_signups_table.sql b/sql/create_signups_table.sql new file mode 100644 index 0000000..fe6bd88 --- /dev/null +++ b/sql/create_signups_table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS signups ( + id INTEGER PRIMARY KEY, + created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')), + email TEXT, + how TEXT, + why TEXT, + links TEXT, + extra TEXT, + decision TEXT +); diff --git a/sql/create_user_db.sql b/sql/create_user_db.sql new file mode 100644 index 0000000..b5890af --- /dev/null +++ b/sql/create_user_db.sql @@ -0,0 +1,45 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')), + username TEXT UNIQUE, + signupid INTEGER, + state TEXT, + admin INTEGER DEFAULT FALSE, + + FOREIGN KEY (signupid) REFERENCES signups(signupid) +); + +CREATE TABLE IF NOT EXISTS emails ( + id INTEGER PRIMARY KEY, + address TEXT UNIQUE, + userid INTEGER, + + FOREIGN KEY (userid) REFERENCES users(userid) +); + +CREATE TABLE IF NOT EXISTS user_notes ( + noteid INTEGER, + userid INTEGER, + + PRIMARY KEY (noteid, userid), + FOREIGN KEY (noteid) REFERENCES notes(noteid), + FOREIGN KEY (userid) REFERENCES users(userid) +); + +CREATE TABLE IF NOT EXISTS notes ( + id INTEGER PRIMARY KEY, + adminid INTEGER, + text TEXT, + created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')), + + FOREIGN KEY (adminid) REFERENCES users(adminid) +); + +CREATE TABLE IF NOT EXISTS signup_notes ( + noteid INTEGER, + signupid INTEGER, + + PRIMARY KEY (noteid, signupid), + FOREIGN KEY (noteid) REFERENCES notes(noteid), + FOREIGN KEY (signupid) REFERENCES signups(signupid) +);