From cf998071264f1be53b2c9c260435876b9290a8ed Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 3 Feb 2023 03:48:40 +0000 Subject: [PATCH] stuff n things --- cmd/contrib/main.go | 3 +- cmd/signup/README.md | 61 +++++++ cmd/signup/main.go | 366 +++++++++++++++++++++++++++++++++++++++++ cmd/signup/thoughts.md | 46 ++++++ go.mod | 7 +- go.sum | 15 +- 6 files changed, 493 insertions(+), 5 deletions(-) create mode 100644 cmd/signup/README.md create mode 100644 cmd/signup/main.go create mode 100644 cmd/signup/thoughts.md diff --git a/cmd/contrib/main.go b/cmd/contrib/main.go index 212eb9f..8b74901 100644 --- a/cmd/contrib/main.go +++ b/cmd/contrib/main.go @@ -246,7 +246,8 @@ func rootCmd() *cobra.Command { rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update") rc.Flags().StringVarP(&delName, "delete", "d", "", "Name of command to delete") - rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command") + // TODO hiding this until i decide i want it or not + //rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command") return rc } diff --git a/cmd/signup/README.md b/cmd/signup/README.md new file mode 100644 index 0000000..690c5f2 --- /dev/null +++ b/cmd/signup/README.md @@ -0,0 +1,61 @@ +The point of this project is to enable signing up for tilde.town via an ssh connection. + +It ought to work something like this: + +```bash + +ssh signup@tilde.town + + + + tilde.town + + +a creature stands before you. what does it look like? + + > a floating gray cube + a calico cat with softly glowing eyes + a squid + something else + +cube: you'd like to sign up for tilde.town, yes? + + > yeah + nah + why should I? + +you say, "yeah" + +cube: by what name should i call you? + + ~cowcow________ + +cube: unfortunately, i already know someone by that name. what else can i call you? + + ~shelf_________ + +cube: excellent, hello ~shelf. have you been invited here by anyone i know? + + > yes, i have an invite code + no + +you say "yes, i have an invite code" + +cube: great. please paste it and press enter: + + _____________ + +``` + +...and so on. + +TODO + +- [ ] make signup user +- [ ] get dummy program to run as ssh handler +- [ ] tcell bootstrapping some kind of interactivity +- [ ] existing username check +- [ ] email validation +- [ ] collect responses +- [ ] create the signup request (on disk? db?) +- [ ] invite system support diff --git a/cmd/signup/main.go b/cmd/signup/main.go new file mode 100644 index 0000000..1ff73bb --- /dev/null +++ b/cmd/signup/main.go @@ -0,0 +1,366 @@ +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 + +*/ + +/* + +is there a way :qa + + +*/ + +type streams struct { + In io.Reader + Out io.Writer + Err io.Writer +} + +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() +} + +func main() { + retcode := 0 + s := &streams{ + In: os.Stdin, + Out: os.Stdout, + Err: os.Stderr, + } + err := _main(os.Args, s) + if err != nil { + retcode = 1 + fmt.Fprintln(os.Stderr, err) + } + os.Exit(retcode) +} diff --git a/cmd/signup/thoughts.md b/cmd/signup/thoughts.md new file mode 100644 index 0000000..b839984 --- /dev/null +++ b/cmd/signup/thoughts.md @@ -0,0 +1,46 @@ +# New commands for signups + +## town-signup + +this binary is run by OpenSSH when a user anywhere in the world runs `ssh join@tilde.town`. It should: + +- collect information about an application from a user +- allow them to edit their responses before submitting +- write a yaml file of their responses to disk + +## review-signups + +this binary is run by town admins in order to review, approve, notate, and reject applications. It should: + +- iterate over signups +- allow fuzzy finding a particular signup +- allow approval using create-user +- allow rejection + - just move signup to archived rejected signup directory +- allow notating an application, ie: + - lock the yaml file for writing + - add notes to the yaml file that can be seen by other admins +- print info about historical signups + +## create-user + +this binary is called by `review-signups` to take a yaml application and create a user on disk. It should: + +- create the user idempotently + - `adduser` + - `usermod` to set group + - calling `add-key` for user +- move the yaml file to an archive directory of approved signups + +## user-key + +this binary helps manage keys for users; basically automating the listing, adding, and removing of public keys for a user. + +- `user-key add ` +- `user-key remove ` + + + + + diff --git a/go.mod b/go.mod index 6d1c289..84ad587 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/AlecAivazis/survey/v2 v2.3.5 github.com/charmbracelet/glamour v0.5.0 + github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c github.com/spf13/cobra v1.5.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -13,6 +14,8 @@ require ( github.com/alecthomas/chroma v0.10.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.5.3 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect @@ -25,12 +28,12 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.9.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/yuin/goldmark v1.4.4 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index b465f7b..37ec13a 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.5.3 h1:b9XQrT6QGbgI7JvZOJXFNczOQeIYbo8BfeSMzt2sAV0= +github.com/gdamore/tcell/v2 v2.5.3/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -47,9 +51,12 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c h1:zIYU4PjQJ4BnYryMmpyizt1Un13V0ToCMXvC05DK8xc= +github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c/go.mod h1:lBUy/T5kyMudFzWUH/C2moN+NlU5qF505vzOyINXuUQ= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= +github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= @@ -70,14 +77,18 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=