package main import ( "database/sql" "fmt" "os" "strconv" "strings" "git.tilde.town/tildetown/town/codes" "git.tilde.town/tildetown/town/towndb" "github.com/charmbracelet/lipgloss" _ "github.com/mattn/go-sqlite3" "github.com/mattn/go-tty" ) // TODO consider local-only help command for renaming and adding emails to account // TODO put colorscheme, prompting stuff into own packages for use in the other commands. would be good to get off of survey. type colorScheme struct { Header func(string) string Subtitle func(string) string Prompt func(string) string Email func(string) string Option func(string) string } func newColorScheme() colorScheme { s2r := func(s lipgloss.Style) func(string) string { return s.Render } c := func(s string) lipgloss.Color { return lipgloss.Color(s) } s := lipgloss.NewStyle return colorScheme{ Header: s2r(s().Bold(true).Foreground(c("#E0B0FF"))), Subtitle: s2r(s().Italic(true).Foreground(c("gray"))), Email: s2r(s().Bold(true).Underline(true)), Prompt: s2r(s().Bold(true).Foreground(c("#00752d"))), Option: s2r(s().Bold(true).Foreground(c("#38747a"))), } } type Prompter struct { cs colorScheme tty *tty.TTY } func NewPrompter(tty *tty.TTY, cs colorScheme) *Prompter { return &Prompter{ cs: cs, tty: tty, } } func (p *Prompter) String(prompt string) (string, error) { fmt.Println("") fmt.Println(p.cs.Prompt(prompt)) fmt.Println(p.cs.Subtitle("(type your answer below and press enter to submit)")) s, err := p.tty.ReadString() if err != nil { return "", fmt.Errorf("couldn't collect input: %w", err) } return s, nil } func (p *Prompter) Select(prompt string, opts []string) (int, error) { fmt.Println() fmt.Println(p.cs.Prompt(prompt)) fmt.Println(p.cs.Subtitle("(pick an option using the corresponding number)")) chosen := -1 for chosen < 0 { fmt.Println() for ix, o := range opts { fmt.Printf("%s: %s\n", p.cs.Option(fmt.Sprintf("%d", ix+1)), o) } r, err := p.tty.ReadRune() if err != nil { return -1, fmt.Errorf("could not collect answer for '%s': %w", prompt, err) } c, err := strconv.Atoi(string(r)) if err != nil { fmt.Println() fmt.Printf("I could not understand '%s'. Try again, please.\n", string(r)) continue } if c > len(opts) || c == 0 { fmt.Println() fmt.Printf("%s is not an option. Try again, please.\n", string(r)) continue } chosen = c - 1 } fmt.Println("") return chosen, nil } func _main() error { db, err := towndb.ConnectDB() if err != nil { return fmt.Errorf("could not connect to database. please let root@tilde.town know about this.") } cs := newColorScheme() fmt.Println(cs.Header("Hi, you have reached the tilde town help desk.")) fmt.Println() fmt.Println("Please check out the options below.") fmt.Printf("If none of them apply to your case, you can email %s. \n", cs.Email("root@tilde.town")) tty, err := tty.Open() if err != nil { return fmt.Errorf("could not open tty: %w", err) } defer tty.Close() p := NewPrompter(tty, cs) options := []string{ "I need to request that a new SSH key be added to my account.", "I have a code from my e-mail to redeem for a new SSH key", "I just want out of here", } c, err := p.Select("What do you need help with?", options) defer func() { fmt.Println() fmt.Println(cs.Header("bye~")) }() switch c { case 0: return collectEmail(db, cs, p) case 1: return redeemCode(db, cs, p) case 2: return nil } return nil } func collectEmail(db *sql.DB, cs colorScheme, p *Prompter) error { fmt.Println(cs.Header("We can send a authorization code to an email associated with your town account.")) email, err := p.String("email to send reset code to?") if err != nil { return err } fmt.Println() fmt.Println(cs.Header("thanks!")) fmt.Println() fmt.Printf("If %s is associated with a town account we'll email an authorization code.\n", cs.Email(email)) mustHave := []string{"@", "."} found := 0 for _, s := range mustHave { if strings.Contains(email, s) { found++ } } if found != len(mustHave) { // TODO log return nil } user, err := towndb.UserForEmail(db, email) if err != nil { // TODO log return err } if user == nil { // TODO log return nil } code := codes.NewCode(email) fmt.Println(code) ac := &towndb.AuthCode{ Code: code, Email: email, } if err = ac.Insert(db); err != nil { // TODO log return err } if err = sendAuthCodeEmail(*ac); err != nil { // TODO log return err } return nil } func redeemCode(db *sql.DB, cs colorScheme, p *Prompter) error { fmt.Println(cs.Header("redeem an auth code and add a new public key")) fmt.Println() c, err := p.String("paste your auth code:") if err != nil { // TODO log return err } parts, err := codes.Decode(c) if err != nil { // TODO log return err } code := parts[0] email := parts[1] fmt.Println(code, email) // TODO verify code // TODO accept key // TODO verify key // TODO add key to .authorized_keys2 // TODO mark used return nil } func main() { err := _main() if err != nil { fmt.Printf("sorry, something went wrong: %s\n", err.Error()) fmt.Println("Please let an admin know by emailing a copy of this error to root@tilde.town") os.Exit(1) } }