town/cmd/signup/main.go

399 lines
8.6 KiB
Go

package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"
"unicode/utf8"
"github.com/rivo/tview"
)
/*
Assumes:
Match User join
ForceCommand /town/src/town/cmd/signup/signup
PubkeyAuthentication no
KbdInteractiveAuthentication no
PasswordAuthentication yes
PermitEmptyPasswords yes
DisableForwarding yes
in sshd_config, and:
auth [success=done default=ignore] pam_succeed_if.so user ingroup join
in /etc/pam.d/sshd
*/
type TownApplication struct {
Email string
// TODO
}
type streams struct {
In io.Reader
Out io.Writer
Err io.Writer
}
func cli(s *streams) error {
pages := tview.NewPages()
mainFlex := tview.NewFlex()
innerFlex := tview.NewFlex()
input := tview.NewTextArea()
title := tview.NewTextView()
title.SetDynamicColors(true)
title.SetTextAlign(tview.AlignCenter)
title.SetText("[purple]the tilde town sign up portal[-]")
msgScroll := tview.NewTextView()
msgScroll.SetDynamicColors(true)
sidebar := tview.NewTextView()
sidebar.SetBorder(true)
sidebar.SetDynamicColors(true)
sidebar.SetText("[-:-:b]pbbt[-:-:-]")
innerFlex.SetDirection(tview.FlexColumn)
innerFlex.AddItem(msgScroll, 0, 2, false)
innerFlex.AddItem(sidebar, 0, 1, false)
mainFlex.SetDirection(tview.FlexRow)
mainFlex.AddItem(title, 1, -1, false)
mainFlex.AddItem(innerFlex, 0, 1, false)
mainFlex.AddItem(input, 5, -1, true)
pages.AddPage("main", mainFlex, true, true)
app := tview.NewApplication()
app.SetRoot(pages, true)
return app.Run()
}
func main() {
s := &streams{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
}
err := cli(s)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
type Prompter struct {
in io.Reader
out io.Writer
width int
}
func (p *Prompter) Say(m string) {
for _, line := range wrap(m, p.width) {
fmt.Fprintln(p.out, line)
}
}
func (p *Prompter) CharSay(c, m string) {
sayPrefix := fmt.Sprintf("%s says: ", c)
prefixWidth := utf8.RuneCountInString(sayPrefix)
width := p.width - prefixWidth
indent := ""
for x := 0; x < utf8.RuneCountInString(sayPrefix); x++ {
indent += " "
}
for i, line := range wrap(m, width) {
if i == 0 {
fmt.Fprintln(p.out, sayPrefix+line)
} else {
fmt.Fprintln(p.out, indent+line)
}
}
}
type InputAnswer struct {
Value string
}
func runeLen(s string) int {
return utf8.RuneCountInString(s)
}
func wrap(s string, width int) []string {
fielded := strings.Fields(s)
out := []string{}
line := ""
for i, field := range fielded {
if runeLen(field)+runeLen(line)+1 < width {
line += field + " "
} else {
out = append(out, line)
line = field + " "
continue
}
if i == len(fielded)-1 {
out = append(out, line)
}
}
return out
}
func (p *Prompter) Confirm(m string) (bool, error) {
result := InputAnswer{}
for {
err := p.Ask(m, &result)
if err != nil {
return false, err
}
val := strings.TrimSpace(result.Value)
switch val {
case "yes", "y":
return true, nil
case "no", "n":
return false, nil
default:
p.Say("sorry, please say y or n")
p.Pause()
}
}
}
func (p *Prompter) Ask(m string, result *InputAnswer) error {
fmt.Fprintf(p.out, "%s ", m)
var val string
var err error
reader := bufio.NewReader(p.in)
for val == "" {
val, err = reader.ReadString('\n')
if err != nil {
return err
}
if val == "" {
p.Say("hmm, what was that?")
p.Pause()
}
}
result.Value = val
return nil
}
func (p *Prompter) AskLong(m string, result *InputAnswer) error {
fmt.Fprintf(p.out, "%s\n", m)
var val []byte
var err error
reader := bufio.NewReader(p.in)
for len(val) == 0 {
val, err = ioutil.ReadAll(reader)
if err != nil {
return err
}
if len(val) == 0 {
fmt.Fprintf(p.out, "%s\n", m)
}
}
result.Value = string(val)
return nil
}
func (p *Prompter) Pause() {
fmt.Fprintln(p.out)
time.Sleep(1 * time.Second)
}
func NewPrompter(width int, s *streams) Prompter {
return Prompter{
in: s.In,
out: s.Out,
width: width,
}
}
type answers struct {
username string
email string
applied time.Time
howDay string
howHeard string
reasons string
plans string
socials string
sshKey string
}
func _main(args []string, s *streams) error {
a := answers{}
p := NewPrompter(80, s)
// disable input buffering
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
// LOL i don't think this will work
//reader := bufio.NewReader(p.in)
//s, _ := reader.ReadString(4)
fmt.Printf("DBG %#v\n", s)
//var b []byte = make([]byte, 100)
//for {
// os.Stdin.Read(b)
// fmt.Println("I got the byte", b, "("+string(b)+")")
//}
var ia InputAnswer
if err := p.AskLong("lol give me stuff hit ctrl+d", &ia); err != nil {
return err
}
fmt.Printf("DBG %#v\n", ia.Value)
p.Say("you are standing in a field.")
p.Pause()
p.Say("there are flowers around you. you are standing in a slight depression and before you is grass touching a purple sky.")
p.Pause()
p.Say("you are not sure how long it's been when a cube about a meter high appears at the top of the hill before you.")
p.Say("the cube's surface is murky but iridescently reflective like an oil slick.")
p.Pause()
p.CharSay("cube", "hello. how is your day going?")
p.Say("(you can type a response and hit enter to talk to the cube)")
p.Pause()
howDay := InputAnswer{}
err := p.Ask("you say:", &howDay)
if err != nil {
return err
}
a.howDay = howDay.Value
p.Say("the cube inclines towards you gently as if nodding.")
p.CharSay("cube", "i see.")
p.Pause()
p.CharSay("cube", "i am guessing that if you are here, you want to be there.")
p.Pause()
p.Say("you blink and are somewhere else.")
p.Pause()
p.Say("the field of flowers is behind you and now you are up on the hill. the cube is next to you.")
p.Pause()
p.Say("you can see clear across a wide open plain. structures large and small dot the landscape. you catch a whiff of a savory smell and can just barely hear voices on the wind.")
p.Pause()
p.CharSay("cube", "the tilde town lies before us. if you want to continue, i'll ask you some questions about acquiring a home down in the town. you'll be free to edit your responses before i carry them off.")
p.Pause()
p.CharSay("cube", "first, i'm curious how you found out about the town?")
p.Pause()
answer := InputAnswer{}
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.howHeard = answer.Value
p.CharSay("cube", "interesting, thanks.")
p.CharSay("cube", "what sort of stuff do you want to get up to on the town?")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.plans = answer.Value
p.CharSay("cube", "thanks.")
p.CharSay("cube", "what do you like about the town?")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.reasons = answer.Value
p.CharSay("cube", "i appreciate it.")
p.CharSay("cube", "can you paste some links to other places you are active online? maybe a homepage or social media profile? if you aren't comfortable sharing or there are none, just say so.")
if err := p.Ask("you say:", &answer); err != nil {
return err
}
a.socials = answer.Value
p.CharSay("cube", "ok, last thing. in order to break ground in the town, you'll need an SSH key. If you don't know what that is, you can check out this link: https://tilde.town/ssh.html .")
p.Pause()
if err := p.AskLong("paste SSH public key; press control+d to submit:", &answer); err != nil {
return err
}
a.sshKey = answer.Value
p.CharSay("cube", "i know that was a lot so i appreciate it. i've got everything written down here. before i carry it off, do you want to review and edit what you wrote?")
confirmed, err := p.Confirm("type y or n: ")
if err != nil {
return err
}
if confirmed {
err := edit(s, &a)
if err != nil {
return err
}
}
fmt.Printf("DBG %#v\n", confirmed)
fmt.Printf("DBG %#v\n", a)
// TODO allow for editing
// TODO write answers to disk
// TODO add a log
// TODO pretty colors
// TODO ascii art
// TODO IP throttling?
return nil
}
func edit(s *streams, a *answers) error {
// TODO make more real
/*
username string
email string
applied time.Time
howDay string
howHeard string
reasons string
plans string
socials string
sshKey string
*/
// TODO add note about tabbing around
app := tview.NewApplication()
form := tview.NewForm().
AddInputField("how did you hear about the town?", a.howHeard, 0, nil, nil).
AddButton("cool i'm good", nil).
AddButton("cancel and discard all this please", func() { app.Stop() })
form.SetBorder(true).SetTitle("edit your stuff").SetTitleAlign(tview.AlignCenter)
return app.SetRoot(form, true).EnableMouse(true).Run()
}