town/cmd/signup/main.go

511 lines
15 KiB
Go
Raw Normal View History

2023-02-03 03:48:40 +00:00
package main
import (
2023-02-10 19:47:09 +00:00
"bytes"
2023-02-03 03:48:40 +00:00
"fmt"
"io"
"os"
"strings"
2023-02-05 22:58:22 +00:00
"github.com/MakeNowJust/heredoc/v2"
"github.com/gdamore/tcell/v2"
2023-02-03 03:48:40 +00:00
"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
*/
2023-02-10 19:47:09 +00:00
const maxInputLength int = 4000
2023-02-03 05:03:03 +00:00
type TownApplication struct {
Email string
2023-02-03 03:48:40 +00:00
2023-02-03 05:03:03 +00:00
// TODO
}
2023-02-03 03:48:40 +00:00
type streams struct {
In io.Reader
Out io.Writer
Err io.Writer
}
2023-02-03 05:03:03 +00:00
func cli(s *streams) error {
pages := tview.NewPages()
mainFlex := tview.NewFlex()
innerFlex := tview.NewFlex()
input := tview.NewTextArea()
2023-02-10 00:22:04 +00:00
input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
input.SetTitle("press ctrl+d to send")
2023-02-10 19:47:09 +00:00
input.SetMaxLength(2000)
2023-02-03 05:03:03 +00:00
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)
2023-02-10 00:22:04 +00:00
msgScroll.SetBackgroundColor(tcell.ColorBlack)
msgScroll.SetTextColor(tcell.ColorWhite)
2023-02-03 05:03:03 +00:00
sidebar := tview.NewTextView()
sidebar.SetBorder(true)
sidebar.SetDynamicColors(true)
2023-02-10 00:22:04 +00:00
sidebar.SetBackgroundColor(tcell.ColorBlack)
sidebar.SetTextColor(tcell.ColorGray)
2023-02-05 22:58:22 +00:00
sidebar.SetText(heredoc.Doc(`
[-:-:b]hey here are some hints[-:-:-]
quit by pressing [-:-:b]ctrl-c[-:-:-]
(your responses won't be saved)
type stuff. send it with [-:-:b]ctrl+d[-:-:-]
try a [-:-:b]verb[-:-:-] using [-:-:b]/[-:-:-] like:
2023-02-10 00:22:04 +00:00
[-:-:b]/nod[-:-:-]
2023-02-05 22:58:22 +00:00
`))
2023-02-03 05:03:03 +00:00
innerFlex.SetDirection(tview.FlexColumn)
2023-02-10 00:22:04 +00:00
innerFlex.AddItem(msgScroll, 0, 3, false)
2023-02-03 05:03:03 +00:00
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)
2023-02-05 22:58:22 +00:00
player := newCharacter("you", "TODO")
2023-02-03 03:48:40 +00:00
2023-02-10 19:47:09 +00:00
currentScene := "start"
2023-02-10 00:22:04 +00:00
sceneTransition := func(text string) {
fmt.Fprintln(msgScroll, heredoc.Doc(`
[purple]----------[-:-:-]
`))
fmt.Fprintln(msgScroll, text)
}
2023-02-10 19:47:09 +00:00
type scene struct {
Description string
InBuff io.ReadWriter
InLength int
2023-02-10 20:05:06 +00:00
Host *character
2023-02-10 19:47:09 +00:00
}
scenes := map[string]*scene{
"start": {
Description: heredoc.Doc(`
2023-02-10 00:22:04 +00:00
You open your eyes.
You're in some kind of workshop.
Wires and computers in various state of disrepair are strewn across
tables and shelves. It smells faintly of burnt cedar.
The wires and components before you slowly drag
themselves into the shape of a small humanoid.
[-:-:b]wire guy says:[-:-:-]
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[-:-:-]
2023-02-10 19:47:09 +00:00
`),
2023-02-10 20:05:06 +00:00
Host: newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
2023-02-10 19:47:09 +00:00
InBuff: bytes.NewBuffer([]byte{}),
2023-02-10 00:22:04 +00:00
},
2023-02-10 19:47:09 +00:00
"nodded": {
Description: heredoc.Doc(`
2023-02-10 00:22:04 +00:00
The workshop fades away. You hear the sound of a dial up modem
in the distance.
Trees spring up out of the ground around you: birches, oaks, maples,
firs, yews, pines, cypresses complete with tiny swamps around their trunks,
junipers, redwoods, cedars, towering palms waving gently in a breeze, eucalyptus,
banyan. the smell is riotous like a canvas with all the colors splashed on. birds
start to sing.
a shrike alights on a branch in front of you.
[-:-:b]the shrike says:[-:-:-]
phweeturpff. how did you find out about the town? did anyone refer you?
just say your answer out loud. when you've said what you want, [-:-:b]/lean[-:-:-]
against a tree.
2023-02-10 19:47:09 +00:00
`),
InBuff: bytes.NewBuffer([]byte{}),
2023-02-10 20:05:06 +00:00
Host: newCharacter("the shrike", "a little grey bird. it has a pretty song."),
2023-02-10 00:22:04 +00:00
},
2023-02-10 19:47:09 +00:00
"leaned": {
Description: heredoc.Doc(`
2023-02-10 00:22:04 +00:00
You sink backwards into the forest. You find yourself floating in darkness.
At the far reaches of your vision you can make out a faint neon grid. Around you
float pieces of consumer electronic appliances from 1980s Earth. A VCR approaches
you and speaks, flapping its tape slot cover with each word.
[-:-:b]the vcr says:[-:-:-]
welcome! thank you for coming this far. just two questions left. what about
tilde town interests you? what kind of stuff might you want to get up to here?
as usual, just say your answer. when you're satisfied, please [-:-:b]/spin[-:-:-]
around in this void.
2023-02-10 19:47:09 +00:00
`),
InBuff: bytes.NewBuffer([]byte{}),
2023-02-10 20:05:06 +00:00
Host: newCharacter("the vcr", "a black and grey VCR from 1991"),
2023-02-10 00:22:04 +00:00
},
2023-02-10 19:47:09 +00:00
"spun": {
Description: heredoc.Doc(`
2023-02-10 00:22:04 +00:00
You realize your eyes have been shut. You open them and, in an instant,
the neon grid and polygons are gone. You're in a convenience store. Outside
it's dark besides a single pool of light coming from a street lamp. it's illuminating
some litter and a rusty, blue 1994 pontiac grand am.
The shelves around you are stocked with products you've never heard of before like
Visible Pants, Petty Burgers, Gentle Rice, Boo Sponge, Power Banjo, Superware, Kneephones,
and Diet Coagulator. A mop is mopping the floor and turns to you.
[-:-:b]the mop says:[-:-:-]
swishy slop. last question. where online can we get to know you? do you have a personal
website or social media presence? we'll take whatever.
say some links and words out loud, you know the drill.
when you're happy you can submit this whole experience by leaving the
store. just [-:-:b]/open[-:-:-] the door.
2023-02-10 19:47:09 +00:00
`),
InBuff: bytes.NewBuffer([]byte{}),
2023-02-10 20:05:06 +00:00
Host: newCharacter("the mop", "a greying mop with a wooden handle."),
2023-02-10 00:22:04 +00:00
},
2023-02-10 19:47:09 +00:00
"done": {
Description: heredoc.Doc(`
2023-02-10 00:22:04 +00:00
thank you for applying to tilde.town!
please be on the look out for an email from [-:-:b]root@tilde.town[-:-:-]
you can [-:-:b]/quit[-:-:-] now
ok bye have a good one~
2023-02-10 19:47:09 +00:00
`),
InBuff: bytes.NewBuffer([]byte{}),
2023-02-10 00:22:04 +00:00
},
}
2023-02-05 22:58:22 +00:00
handleInput := func(msg string) {
2023-02-10 00:22:04 +00:00
trimmed := strings.TrimSpace(msg)
if trimmed == "" {
return
}
if strings.HasPrefix(trimmed, "/") {
split := strings.Split(trimmed, " ")
if len(split) > 0 {
trimmed = split[0]
}
switch strings.TrimPrefix(trimmed, "/") {
case "quit":
2023-02-10 20:05:06 +00:00
app.Stop()
2023-02-10 19:47:09 +00:00
case "look":
fmt.Fprintln(msgScroll, "")
fmt.Fprintln(msgScroll, scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
// TODO refactor into a state machine
case "nod":
2023-02-10 19:47:09 +00:00
if currentScene != "start" {
2023-02-10 00:22:04 +00:00
return
}
2023-02-10 20:05:06 +00:00
if scenes[currentScene].InLength == 0 {
fmt.Fprintln(msgScroll,
scenes[currentScene].Host.Say("i'm sorry, before going further could you share an email with me?"))
return
2023-02-10 19:47:09 +00:00
}
2023-02-10 20:05:06 +00:00
currentScene = "nodded"
sceneTransition(scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
case "lean":
2023-02-10 19:47:09 +00:00
if currentScene != "nodded" {
2023-02-10 00:22:04 +00:00
return
}
2023-02-10 20:05:06 +00:00
if scenes[currentScene].InLength == 0 {
fmt.Fprintln(msgScroll,
scenes[currentScene].Host.Say("phweeturpff"))
return
}
2023-02-10 19:47:09 +00:00
currentScene = "leaned"
sceneTransition(scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
case "spin":
2023-02-10 19:47:09 +00:00
if currentScene != "leaned" {
2023-02-10 00:22:04 +00:00
return
}
2023-02-10 20:05:06 +00:00
if scenes[currentScene].InLength == 0 {
fmt.Fprintln(msgScroll,
scenes[currentScene].Host.Say("hmm did you say something?"))
return
}
2023-02-10 19:47:09 +00:00
currentScene = "spun"
sceneTransition(scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
case "open":
2023-02-10 19:47:09 +00:00
if currentScene != "spun" {
2023-02-10 00:22:04 +00:00
return
}
2023-02-10 20:05:06 +00:00
if scenes[currentScene].InLength == 0 {
fmt.Fprintln(msgScroll,
scenes[currentScene].Host.Say("just the one last thing please"))
return
}
2023-02-10 19:47:09 +00:00
currentScene = "done"
sceneTransition(scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
}
return
2023-02-05 22:58:22 +00:00
}
fmt.Fprintln(msgScroll, player.Say(msg))
2023-02-10 19:47:09 +00:00
fmt.Fprintln(scenes[currentScene].InBuff, msg)
scenes[currentScene].InLength += len(msg)
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyCtrlD:
handleInput(input.GetText())
input.SetText("", false)
return nil
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
return event
})
2023-02-03 03:48:40 +00:00
2023-02-10 00:22:04 +00:00
app.SetAfterDrawFunc(func(_ tcell.Screen) {
2023-02-10 19:47:09 +00:00
fmt.Fprintln(msgScroll, scenes[currentScene].Description)
2023-02-10 00:22:04 +00:00
app.SetAfterDrawFunc(nil)
})
2023-02-05 22:58:22 +00:00
return app.Run()
2023-02-03 03:48:40 +00:00
}
2023-02-10 00:22:04 +00:00
/*
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.
one idea but i'm scratchin:
opening:
It's dark. Before you a button appears. It's a pink circle with a ~ at the center. It looks like a button. You could press it.
/press button
A thin seam splits the darkness from the top and buttom of the button. The darkness splits apart and you're standing in a forest clearing. Birds are singing in strange octaves.
A cube is here, floating.
cube: hello! how are you? you can respond to me by typing anything and pressing "ctrl+d"
<input>
cube: thank you for telling me. i'm just a program so i can't really understand. i hope you forgive me.
cube: i'm here to help you apply for a homestead over there in tilde town.
the cube inclines in the direction of what looks like a far off town.
cube: first, what is an email address we can use to contact you?
<input>
cube: cool, thank you. that's <email> right? if so, you can /nod to confirm. otherwise just tell me again what your email is.
<nod>
cube: excellent. next, you'll talk to
*/
2023-02-05 22:58:22 +00:00
type character struct {
Name string
Description string
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
func newCharacter(name, description string) *character {
return &character{
Name: name,
Description: description,
2023-02-03 03:48:40 +00:00
}
}
2023-02-05 22:58:22 +00:00
func (c *character) Say(msg string) string {
verb := "says"
if c.Name == "you" {
verb = "say"
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
return fmt.Sprintf("[-:-:b]%s[-:-:-] %s: '%s'",
c.Name,
verb,
strings.TrimSpace(msg))
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
func main() {
s := &streams{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
2023-02-03 03:48:40 +00:00
}
2023-02-05 22:58:22 +00:00
err := cli(s)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
2023-02-03 03:48:40 +00:00
}
}
2023-02-10 00:22:04 +00:00
/*
From the start point the player advances the "form" by performing a tactile action. for now it's a button press but it could be something more interesting later. Keeping this as a one room, limited-verb thing is good for accessibility.
so the script is a list of strings that advance by tactility...is it dialogue? or scene description? i think i should fake dialogue /as/ scene description.
You open your eyes. You're standing in a small room. It's dim, lit softly by a mix of candles and electro-luminescent wire. Computers.
*/
2023-02-05 22:58:22 +00:00
/*
2023-02-03 03:48:40 +00:00
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 {
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?")
}
2023-02-05 22:58:22 +00:00
*/