Compare commits

..

2 Commits

Author SHA1 Message Date
vilmibm 45dd8efbae unfuck input collection 2023-02-13 06:13:07 +00:00
vilmibm 462e8772ec save answers to file, some cleanup (broken) 2023-02-13 01:28:45 +00:00
3 changed files with 182 additions and 181 deletions

View File

@ -1,61 +1,79 @@
The point of this project is to enable signing up for tilde.town via an ssh connection. # town signup
The point of this command is to enable signing up for tilde.town via an ssh connection. It is designed to be run when `join@tilde.town` is SSH'd to.
It ought to work something like this: ## to-dos
```bash - [ ] finish this command
- [x] interactive guts
ssh signup@tilde.town - [x] logging
- [ ] write answers to disk
<ascii art> - [ ] splash screen - put off
- [ ] easter egg commands - put off
tilde.town - [ ] inactivity timer(?) - put off
- [ ] review tool
- [ ] iterate over answers
- [ ] accept
- [ ] notate
- [ ] reject
- [ ] send email with directions on key upload
- [ ] actual account creation
- [ ] accept key
- [ ] accept username
- [ ] create user
- [ ] backlog
- [ ] get a manual dump from psql of json
- [ ] convert into files in the review directory
a creature stands before you. what does it look like? ## configuration
> a floating gray cube It assumes, in `sshd_config`:
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:
_____________
``` ```
Match User join
ForceCommand /town/src/town/cmd/signup/signup
PubkeyAuthentication no
KbdInteractiveAuthentication no
PasswordAuthentication yes
PermitEmptyPasswords yes
DisableForwarding yes
```
...and so on. and in `/etc/pam.d/sshd`:
TODO ```
auth [success=done default=ignore] pam_succeed_if.so user ingroup join
```
- [ ] make signup user ## initial thoughts
- [ ] get dummy program to run as ssh handler
- [ ] tcell bootstrapping some kind of interactivity I need a script for how this interaction should go. It should feel more organic and fluid than just filling out a form. It shouldn't, however, take more than 10 minutes to get through stuff. The end result is to collect:
- [ ] existing username check
- [ ] email validation - an email address
- [ ] collect responses - how a user found out about the town
- [ ] create the signup request (on disk? db?) - if they have a referral from an existing member
- [ ] invite system support - what interests them about the town
- any links to personal websites or social media
A given is that the applicant is interacting with a "host" that guides them through the process. Should there be more than one host?
How many rooms should there be? 1? more?
in terms of data collection, I intend to just save the transcript of their interaction instead of more structured data. The only real structured data are email and referral.
another idea/thought/dream:
the whole point of this is to spatialize a form. it's a form! a set of questions and corresponding textarea elements. but in order to trigger self reflection -- as well as a feeling of being seen by another (in a good way) -- i want to turn the form into a shape. i'm thinking about this like the room in Eclipse Penumbra:
(For the Hollow Head was drug paraphenalia you could walk into. The building itself was the syringe, or the hookah, or the sniff-tube)
so, spatialized, every room is a question. the rooms take shape as a linear script the user moves through by:
1. answering a question
2. executing a verb
and finally at the end, a verb to confirm submission.
## author
vilmibm

View File

@ -1,51 +1,79 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"io" "log"
"os" "os"
"path"
"strings" "strings"
"time"
"encoding/json"
"github.com/MakeNowJust/heredoc/v2" "github.com/MakeNowJust/heredoc/v2"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
/* const (
Assumes: maxInputLength int = 10000
signupDirectory string = "/town/signups"
logDir string = "/town/var/signup"
)
Match User join type scene struct {
ForceCommand /town/src/town/cmd/signup/signup Name string
PubkeyAuthentication no Description string
KbdInteractiveAuthentication no Key string
PasswordAuthentication yes Host *character
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
*/
const maxInputLength int = 4000
type TownApplication struct {
Email string
HowFound string
Why string
Where string
} }
type streams struct { type townApplication struct {
In io.Reader When time.Time
Out io.Writer Answers map[string]string
Err io.Writer
} }
func cli(s *streams) error { type character struct {
Name string
Description string
}
func newCharacter(name, description string) *character {
return &character{
Name: name,
Description: description,
}
}
func (c *character) Say(msg string) string {
verb := "says"
if c.Name == "you" {
verb = "say"
}
return fmt.Sprintf("[-:-:b]%s[-:-:-] %s: '%s'",
c.Name,
verb,
strings.TrimSpace(msg))
}
func main() {
logFile := path.Join(logDir, fmt.Sprintf("%d", time.Now().Unix()))
logF, err := os.Create(logFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
logger := log.New(logF, "", log.Ldate|log.Ltime)
err = _main(logger)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
func _main(l *log.Logger) error {
l.Println("starting a session")
pages := tview.NewPages() pages := tview.NewPages()
mainFlex := tview.NewFlex() mainFlex := tview.NewFlex()
innerFlex := tview.NewFlex() innerFlex := tview.NewFlex()
@ -98,12 +126,33 @@ func cli(s *streams) error {
player := newCharacter("you", "TODO") player := newCharacter("you", "TODO")
type scene struct { townApp := &townApplication{
Name string Answers: map[string]string{},
Description string }
InBuff io.ReadWriter
InLength int save := func() {
Host *character 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
}
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))
return
}
} }
scenes := []*scene{ scenes := []*scene{
@ -123,10 +172,11 @@ func cli(s *streams) error {
hello, welcome to the application for membership in tilde town. hello, welcome to the application for membership in tilde town.
first, please let me know what a good [-:-:b]email address[-:-:-] is for you? first, please let me know what a good [-:-:b]email address[-:-:-] is for you?
just say it out loud. as many times as you need. to get it right. just say it out loud. as many times as you need. to get it right.
when you're ready to move on, [-:-:b]/nod[-:-:-] when you're ready to move on, [-:-:b]/nod[-:-:-]
`), `),
Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"), Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
InBuff: bytes.NewBuffer([]byte{}), Key: "email",
}, },
{ {
Name: "nodded", Name: "nodded",
@ -148,8 +198,8 @@ func cli(s *streams) error {
just say your answer out loud. when you've said what you want, [-:-:b]/lean[-:-:-] just say your answer out loud. when you've said what you want, [-:-:b]/lean[-:-:-]
against a tree. against a tree.
`), `),
InBuff: bytes.NewBuffer([]byte{}), Key: "how",
Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."), Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."),
}, },
{ {
Name: "leaned", Name: "leaned",
@ -167,8 +217,8 @@ func cli(s *streams) error {
as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-] as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-]
around in this void. around in this void.
`), `),
InBuff: bytes.NewBuffer([]byte{}), Key: "why",
Host: newCharacter("the vcr", "a black and grey VCR from 1991"), Host: newCharacter("the vcr", "a black and grey VCR from 1991"),
}, },
{ {
Name: "spun", Name: "spun",
@ -191,11 +241,11 @@ func cli(s *streams) error {
when you're happy you can submit this whole experience by leaving the when you're happy you can submit this whole experience by leaving the
store. just [-:-:b]/open[-:-:-] the door. store. just [-:-:b]/open[-:-:-] the door.
`), `),
InBuff: bytes.NewBuffer([]byte{}), Key: "where",
Host: newCharacter("the mop", "a greying mop with a wooden handle."), Host: newCharacter("the mop", "a greying mop with a wooden handle."),
}, },
{ {
Name: "spun", Name: "done",
Description: heredoc.Doc(` Description: heredoc.Doc(`
thank you for applying to tilde.town! thank you for applying to tilde.town!
@ -205,7 +255,7 @@ func cli(s *streams) error {
ok bye have a good one~ ok bye have a good one~
`), `),
InBuff: bytes.NewBuffer([]byte{}), Key: "extra",
}, },
} }
@ -216,7 +266,7 @@ func cli(s *streams) error {
if currentScene.Name != fromScene { if currentScene.Name != fromScene {
return return
} }
if currentScene.InLength == 0 { if len(townApp.Answers[currentScene.Key]) == 0 {
fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg)) fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg))
return return
} }
@ -231,22 +281,22 @@ func cli(s *streams) error {
} }
handleInput := func(msg string) { handleInput := func(msg string) {
trimmed := strings.TrimSpace(msg) msg = strings.TrimSpace(msg)
if trimmed == "" { if msg == "" {
return return
} }
if strings.HasPrefix(trimmed, "/") { if strings.HasPrefix(msg, "/") {
split := strings.Split(trimmed, " ") split := strings.Split(msg, " ")
if len(split) > 0 { if len(split) > 0 {
trimmed = split[0] msg = split[0]
} }
switch strings.TrimPrefix(trimmed, "/") { switch strings.TrimPrefix(msg, "/") {
case "quit": case "quit":
l.Println("got /quit")
app.Stop() app.Stop()
case "look": case "look":
fmt.Fprintln(msgScroll, "") fmt.Fprintln(msgScroll, "")
fmt.Fprintln(msgScroll, currentScene.Description) fmt.Fprintln(msgScroll, currentScene.Description)
// TODO refactor into a state machine
case "nod": case "nod":
advanceScene("start", advanceScene("start",
"i'm sorry, before going further could you share an email with me?") "i'm sorry, before going further could you share an email with me?")
@ -256,12 +306,17 @@ func cli(s *streams) error {
advanceScene("leaned", "hmm did you say something?") advanceScene("leaned", "hmm did you say something?")
case "open": case "open":
advanceScene("spun", "just the one last thing please") advanceScene("spun", "just the one last thing please")
save()
} }
return return
} }
if len(townApp.Answers[currentScene.Key]) > 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)) fmt.Fprintln(msgScroll, player.Say(msg))
fmt.Fprintln(currentScene.InBuff, msg) townApp.Answers[currentScene.Key] += ("\n" + msg)
currentScene.InLength += len(msg)
} }
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@ -282,70 +337,3 @@ func cli(s *streams) error {
return app.Run() return app.Run()
} }
/*
I need a script for how this interaction should go. It should feel more organic and fluid than just filling out a form. It shouldn't, however, take more than 10 minutes to get through stuff. The end result is to collect:
- an email address
- how a user found out about the town
- if they have a referral from an existing member
- what interests them about the town
- any links to personal websites or social media
A given is that the applicant is interacting with a "host" that guides them through the process. Should there be more than one host?
How many rooms should there be? 1? more?
in terms of data collection, I intend to just save the transcript of their interaction instead of more structured data. The only real structured data are email and referral.
another idea/thought/dream:
the whole point of this is to spatialize a form. it's a form! a set of questions and corresponding textarea elements. but in order to trigger self reflection -- as well as a feeling of being seen by another (in a good way) -- i want to turn the form into a shape. i'm thinking about this like the room in Eclipse Penumbra:
(For the Hollow Head was drug paraphenalia you could walk into. The building itself was the syringe, or the hookah, or the sniff-tube)
so, spatialized, every room is a question. the rooms take shape as a linear script the user moves through by:
1. answering a question
2. executing a verb
and finally at the end, a verb to confirm submission.
*/
type character struct {
Name string
Description string
}
func newCharacter(name, description string) *character {
return &character{
Name: name,
Description: description,
}
}
func (c *character) Say(msg string) string {
verb := "says"
if c.Name == "you" {
verb = "say"
}
return fmt.Sprintf("[-:-:b]%s[-:-:-] %s: '%s'",
c.Name,
verb,
strings.TrimSpace(msg))
}
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)
}
}

View File

@ -1,4 +1,4 @@
# New commands for signups # New commands for signups, rough draft
## town-signup ## town-signup
@ -39,8 +39,3 @@ this binary helps manage keys for users; basically automating the listing, addin
- `user-key add <username> <keyfile` - `user-key add <username> <keyfile`
- `user-key list <username>` - `user-key list <username>`
- `user-key remove <username>` - `user-key remove <username>`