Compare commits

..

No commits in common. "45dd8efbaeea82ab9bd4d75076fdcc952eeac501" and "4a881d3b8bba19b5bef80b3db57ec77bf00ea59d" have entirely different histories.

3 changed files with 181 additions and 182 deletions

View File

@ -1,79 +1,61 @@
# town signup The point of this project is to enable signing up for tilde.town via an ssh connection.
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.
## to-dos It ought to work something like this:
- [ ] finish this command ```bash
- [x] interactive guts
- [x] logging ssh signup@tilde.town
- [ ] write answers to disk
- [ ] splash screen - put off <ascii art>
- [ ] easter egg commands - put off
- [ ] inactivity timer(?) - put off tilde.town
- [ ] 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
## configuration a creature stands before you. what does it look like?
It assumes, in `sshd_config`: > 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:
_____________
``` ```
Match User join
ForceCommand /town/src/town/cmd/signup/signup
PubkeyAuthentication no
KbdInteractiveAuthentication no
PasswordAuthentication yes
PermitEmptyPasswords yes
DisableForwarding yes
```
and in `/etc/pam.d/sshd`: ...and so on.
``` TODO
auth [success=done default=ignore] pam_succeed_if.so user ingroup join
```
## initial thoughts - [ ] make signup user
- [ ] get dummy program to run as ssh handler
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: - [ ] tcell bootstrapping some kind of interactivity
- [ ] existing username check
- an email address - [ ] email validation
- how a user found out about the town - [ ] collect responses
- if they have a referral from an existing member - [ ] create the signup request (on disk? db?)
- what interests them about the town - [ ] invite system support
- 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,79 +1,51 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"log" "io"
"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 ( /*
maxInputLength int = 10000 Assumes:
signupDirectory string = "/town/signups"
logDir string = "/town/var/signup"
)
type scene struct { Match User join
Name string ForceCommand /town/src/town/cmd/signup/signup
Description string PubkeyAuthentication no
Key string KbdInteractiveAuthentication no
Host *character 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 townApplication struct { type streams struct {
When time.Time In io.Reader
Answers map[string]string Out io.Writer
Err io.Writer
} }
type character struct { func cli(s *streams) error {
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()
@ -126,33 +98,12 @@ func _main(l *log.Logger) error {
player := newCharacter("you", "TODO") player := newCharacter("you", "TODO")
townApp := &townApplication{ type scene struct {
Answers: map[string]string{}, Name string
} Description string
InBuff io.ReadWriter
save := func() { InLength int
townApp.When = time.Now() Host *character
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{
@ -172,11 +123,10 @@ func _main(l *log.Logger) 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"),
Key: "email", InBuff: bytes.NewBuffer([]byte{}),
}, },
{ {
Name: "nodded", Name: "nodded",
@ -198,8 +148,8 @@ func _main(l *log.Logger) 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.
`), `),
Key: "how", InBuff: bytes.NewBuffer([]byte{}),
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",
@ -217,8 +167,8 @@ func _main(l *log.Logger) 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.
`), `),
Key: "why", InBuff: bytes.NewBuffer([]byte{}),
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",
@ -241,11 +191,11 @@ func _main(l *log.Logger) 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.
`), `),
Key: "where", InBuff: bytes.NewBuffer([]byte{}),
Host: newCharacter("the mop", "a greying mop with a wooden handle."), Host: newCharacter("the mop", "a greying mop with a wooden handle."),
}, },
{ {
Name: "done", Name: "spun",
Description: heredoc.Doc(` Description: heredoc.Doc(`
thank you for applying to tilde.town! thank you for applying to tilde.town!
@ -255,7 +205,7 @@ func _main(l *log.Logger) error {
ok bye have a good one~ ok bye have a good one~
`), `),
Key: "extra", InBuff: bytes.NewBuffer([]byte{}),
}, },
} }
@ -266,7 +216,7 @@ func _main(l *log.Logger) error {
if currentScene.Name != fromScene { if currentScene.Name != fromScene {
return return
} }
if len(townApp.Answers[currentScene.Key]) == 0 { if currentScene.InLength == 0 {
fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg)) fmt.Fprintln(msgScroll, currentScene.Host.Say(sorryMsg))
return return
} }
@ -281,22 +231,22 @@ func _main(l *log.Logger) error {
} }
handleInput := func(msg string) { handleInput := func(msg string) {
msg = strings.TrimSpace(msg) trimmed := strings.TrimSpace(msg)
if msg == "" { if trimmed == "" {
return return
} }
if strings.HasPrefix(msg, "/") { if strings.HasPrefix(trimmed, "/") {
split := strings.Split(msg, " ") split := strings.Split(trimmed, " ")
if len(split) > 0 { if len(split) > 0 {
msg = split[0] trimmed = split[0]
} }
switch strings.TrimPrefix(msg, "/") { switch strings.TrimPrefix(trimmed, "/") {
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?")
@ -306,17 +256,12 @@ func _main(l *log.Logger) 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))
townApp.Answers[currentScene.Key] += ("\n" + msg) fmt.Fprintln(currentScene.InBuff, msg)
currentScene.InLength += len(msg)
} }
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
@ -337,3 +282,70 @@ func _main(l *log.Logger) 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, rough draft # New commands for signups
## town-signup ## town-signup
@ -39,3 +39,8 @@ 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>`