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
ssh signup@tilde.town
<ascii art>
tilde.town
- [ ] finish this command
- [x] interactive guts
- [x] logging
- [ ] write answers to disk
- [ ] splash screen - put off
- [ ] easter egg commands - put off
- [ ] 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
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:
_____________
It assumes, in `sshd_config`:
```
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
- [ ] 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
## initial thoughts
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.
## author
vilmibm

View File

@ -1,51 +1,79 @@
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path"
"strings"
"time"
"encoding/json"
"github.com/MakeNowJust/heredoc/v2"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
/*
Assumes:
const (
maxInputLength int = 10000
signupDirectory string = "/town/signups"
logDir string = "/town/var/signup"
)
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
*/
const maxInputLength int = 4000
type TownApplication struct {
Email string
HowFound string
Why string
Where string
type scene struct {
Name string
Description string
Key string
Host *character
}
type streams struct {
In io.Reader
Out io.Writer
Err io.Writer
type townApplication struct {
When time.Time
Answers map[string]string
}
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()
mainFlex := tview.NewFlex()
innerFlex := tview.NewFlex()
@ -98,12 +126,33 @@ func cli(s *streams) error {
player := newCharacter("you", "TODO")
type scene struct {
Name string
Description string
InBuff io.ReadWriter
InLength int
Host *character
townApp := &townApplication{
Answers: map[string]string{},
}
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
}
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{
@ -123,10 +172,11 @@ func cli(s *streams) error {
hello, welcome to the application for membership in tilde town.
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.
when you're ready to move on, [-:-:b]/nod[-:-:-]
`),
Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
InBuff: bytes.NewBuffer([]byte{}),
Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
Key: "email",
},
{
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[-:-:-]
against a tree.
`),
InBuff: bytes.NewBuffer([]byte{}),
Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."),
Key: "how",
Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."),
},
{
Name: "leaned",
@ -167,8 +217,8 @@ func cli(s *streams) error {
as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-]
around in this void.
`),
InBuff: bytes.NewBuffer([]byte{}),
Host: newCharacter("the vcr", "a black and grey VCR from 1991"),
Key: "why",
Host: newCharacter("the vcr", "a black and grey VCR from 1991"),
},
{
Name: "spun",
@ -191,11 +241,11 @@ func cli(s *streams) error {
when you're happy you can submit this whole experience by leaving the
store. just [-:-:b]/open[-:-:-] the door.
`),
InBuff: bytes.NewBuffer([]byte{}),
Host: newCharacter("the mop", "a greying mop with a wooden handle."),
Key: "where",
Host: newCharacter("the mop", "a greying mop with a wooden handle."),
},
{
Name: "spun",
Name: "done",
Description: heredoc.Doc(`
thank you for applying to tilde.town!
@ -205,7 +255,7 @@ func cli(s *streams) error {
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 {
return
}
if currentScene.InLength == 0 {
if len(townApp.Answers[currentScene.Key]) == 0 {
fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg))
return
}
@ -231,22 +281,22 @@ func cli(s *streams) error {
}
handleInput := func(msg string) {
trimmed := strings.TrimSpace(msg)
if trimmed == "" {
msg = strings.TrimSpace(msg)
if msg == "" {
return
}
if strings.HasPrefix(trimmed, "/") {
split := strings.Split(trimmed, " ")
if strings.HasPrefix(msg, "/") {
split := strings.Split(msg, " ")
if len(split) > 0 {
trimmed = split[0]
msg = split[0]
}
switch strings.TrimPrefix(trimmed, "/") {
switch strings.TrimPrefix(msg, "/") {
case "quit":
l.Println("got /quit")
app.Stop()
case "look":
fmt.Fprintln(msgScroll, "")
fmt.Fprintln(msgScroll, currentScene.Description)
// TODO refactor into a state machine
case "nod":
advanceScene("start",
"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?")
case "open":
advanceScene("spun", "just the one last thing please")
save()
}
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(currentScene.InBuff, msg)
currentScene.InLength += len(msg)
townApp.Answers[currentScene.Key] += ("\n" + msg)
}
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@ -282,70 +337,3 @@ func cli(s *streams) error {
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
@ -39,8 +39,3 @@ this binary helps manage keys for users; basically automating the listing, addin
- `user-key add <username> <keyfile`
- `user-key list <username>`
- `user-key remove <username>`