Compare commits

...

46 Commits
trunk ... trunk

Author SHA1 Message Date
nate smith 51a2b386ea cleanup 2024-09-01 15:34:29 -05:00
nate smith 7cb2fd67d1 rsvp form 2024-09-01 15:32:45 -05:00
nate smith ebadba99af towncon wip 2024-09-01 15:24:33 -05:00
nate smith b3294e880f cleanup 2024-09-01 15:17:13 -05:00
nate smith 7b9ccf0ff2 stuff
- WIP toward towncon command
- upgrade lipgloss
- fix fallout from lipgloss update
2024-09-01 13:13:19 -05:00
nate smith 5750265818 start on con command 2024-08-30 20:46:16 -05:00
vilmibm 83b9dd8f21 stuff. WIP tma tool 2024-08-30 02:06:24 +00:00
vilmibm 5082290d15 add tma 2024-05-06 01:53:48 +00:00
vilmibm 65c9f89f75 fix external mailing 2024-04-16 00:44:55 +00:00
vilmibm 051326330a fix smtp host 2024-04-08 17:25:52 +00:00
vilmibm ca33731826 no, c 2023-11-04 05:11:36 +00:00
vilmibm 81c9dede67 tweak 2023-11-04 05:09:33 +00:00
vilmibm 4958407856 add Makefile 2023-11-04 05:08:46 +00:00
vilmibm 9bb456bc17 add newline to keyfile if needed 2023-11-01 20:59:49 +00:00
vilmibm 69ad6a384a idk 2023-11-01 20:36:53 +00:00
vilmibm 4284fb4048 logger 2023-11-01 03:42:43 +00:00
vilmibm 100643d8fc further the rearranging 2023-11-01 03:25:26 +00:00
vilmibm 0764534fed rearrange things, add locking writer 2023-11-01 02:44:22 +00:00
vilmibm 69666edefa success note 2023-10-26 22:36:48 +00:00
vilmibm add129826a assume prompts are required 2023-10-26 22:32:30 +00:00
vilmibm 90808c1ce0 bug fixes, ready for external qa 2023-10-25 19:24:01 +00:00
vilmibm 17d39483fb fix logging 2023-10-25 17:52:04 +00:00
vilmibm 418e4a4a14 logging 2023-10-25 17:42:46 +00:00
vilmibm 79dc987c61 minor 2023-10-25 08:44:20 +00:00
vilmibm ba1a1319e3 sigh 2023-10-25 08:42:31 +00:00
vilmibm cbc868ae35 oops 2023-10-25 08:34:58 +00:00
vilmibm 940779876c oops 2023-10-25 08:34:32 +00:00
vilmibm f53b2721cb use helper 2023-10-25 08:33:25 +00:00
vilmibm b1ff57ba58 fill in emailtouser 2023-10-25 08:24:23 +00:00
vilmibm be5020ad28 WIP retooling bc of permission issue 2023-10-25 04:39:13 +00:00
vilmibm 9bea4257c1 notes 2023-10-25 03:52:54 +00:00
vilmibm 2a07a0e200 mark code used 2023-10-25 03:03:36 +00:00
vilmibm 7255ee691e minor 2023-10-25 02:48:01 +00:00
vilmibm 44686ad536 add new appendkeyfile helper 2023-10-25 02:41:16 +00:00
vilmibm 529e14158a forgot to add these 2023-10-25 02:41:02 +00:00
vilmibm bf244101e6 finish code validation, start on key handling 2023-10-25 01:40:37 +00:00
vilmibm 6fa11aba8e code processing, TODOs 2023-10-24 19:23:35 +00:00
vilmibm 5c2142f6e7 auth code db methods 2023-10-24 19:17:16 +00:00
vilmibm 92faddd079 refactor prompting, start on redeemCode 2023-10-24 19:10:13 +00:00
vilmibm 6950ba7109 send auth code email 2023-10-24 18:58:34 +00:00
vilmibm a3b13d21b3 creating auth codes in db 2023-10-24 06:22:21 +00:00
vilmibm 5c6f4cce19 ref 2023-10-24 05:16:51 +00:00
vilmibm 2a966bf842 ask db about users 2023-10-24 05:15:04 +00:00
vilmibm 880511a79a add color and email prompt 2023-10-21 04:58:30 +00:00
vilmibm 0884ba2ff6 fix glaring upsetting error in signup copy 2023-10-18 00:48:47 +00:00
vilmibm d0b79f3e7c start on bare bones help SSH command 2023-10-18 00:48:37 +00:00
39 changed files with 1440 additions and 134 deletions

13
.gitignore vendored
View File

@ -1,10 +1,15 @@
*.swp
bin/
cmd/launcher/launcher
cmd/request/request
cmd/contrib/contrib
cmd/visit/visit
cmd/signup/signup
cmd/review/review
cmd/welcome/welcome
cmd/createkeyfile/createkeyfile
cmd/registeruser/registeruser
cmd/stats/stats
external/cmd/signup/signup
external/cmd/welcome/welcome
external/cmd/help/help
external/cmd/helpers/emailtouser/emailtouser
external/cmd/helpers/createkeyfile/createkeyfile
external/cmd/helpers/registeruser/registeruser
external/cmd/helpers/appendkeyfile/appendkeyfile

71
Makefile 100644
View File

@ -0,0 +1,71 @@
all: cmds external
install: all
cp bin/tma /town/bin/
cp bin/launcher /usr/local/bin/town
cp bin/stats /town/bin/
cp bin/contrib /town/bin/
cp bin/con /town/bin/
cp bin/request /town/bin/
cp bin/appendkeyfile /town/bin/
cp bin/createkeyfile /town/bin/
cp bin/emailtouser /town/bin/
cp bin/registeruser /town/bin/
cp bin/help /town/bin/external/
cp bin/welcome /town/bin/external/
cp bin/signup /town/bin/external/
cp bin/review /town/bin/
clean:
rm -rf bin
external: bin/help bin/welcome bin/signup exthelpers
bin/help: external/cmd/help/main.go bin
go build -o bin/help ./external/cmd/help
bin/welcome: external/cmd/welcome/main.go bin
go build -o bin/welcome ./external/cmd/welcome
bin/signup: external/cmd/signup/main.go bin
go build -o bin/signup ./external/cmd/signup
exthelpers: bin/appendkeyfile bin/createkeyfile bin/emailtouser bin/registeruser
bin/appendkeyfile: external/cmd/helpers/appendkeyfile/main.go bin
go build -o bin/appendkeyfile ./external/cmd/helpers/appendkeyfile
bin/createkeyfile: external/cmd/helpers/createkeyfile/main.go bin
go build -o bin/createkeyfile ./external/cmd/helpers/createkeyfile
bin/emailtouser: external/cmd/helpers/emailtouser/main.go bin
go build -o bin/emailtouser ./external/cmd/helpers/emailtouser
bin/registeruser: external/cmd/helpers/registeruser/main.go bin
go build -o bin/registeruser ./external/cmd/helpers/registeruser
cmds: bin/launcher bin/stats bin/con bin/contrib bin/request bin/review bin/tma
bin/launcher: cmd/launcher/main.go bin
go build -o bin/launcher ./cmd/launcher
bin/stats: cmd/stats/main.go bin
go build -o bin/stats ./cmd/stats
bin/contrib: cmd/contrib/main.go bin
go build -o bin/contrib ./cmd/contrib
bin/con: cmd/towncon/main.go bin
go build -o bin/con ./cmd/towncon
bin/request: cmd/request/main.go bin
go build -o bin/request ./cmd/request
bin/review: cmd/review/main.go bin
go build -o bin/review ./cmd/review
bin/tma: cmd/tma/main.go bin
go build -o bin/tma ./cmd/tma
bin:
mkdir -p bin

View File

@ -3,7 +3,6 @@ package main
import (
"errors"
"fmt"
"math/rand"
"os"
"os/user"
"path"
@ -71,8 +70,6 @@ func validExec(execPath string) error {
}
func submit(opts *contribOpts) error {
rand.Seed(time.Now().UTC().UnixNano())
var cmdName string
var category string
var shortDesc string

View File

@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("TODO")
}

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"git.tilde.town/tildetown/town/email"
"git.tilde.town/tildetown/town/invites"
@ -31,20 +32,20 @@ See you on the server,
func loadPassword() (string, error) {
f, err := os.Open("/town/docs/smtp.pw")
if err != nil {
return "", err
return "", fmt.Errorf("could not open smtp password file: %w", err)
}
pw := make([]byte, 100)
n, err := f.Read(pw)
if err != nil {
return "", err
return "", fmt.Errorf("could not read smtp password file: %w", err)
}
if n == 0 {
return "", errors.New("read nothing")
return "", errors.New("smtp password file was empty")
}
return string(pw[0:n]), nil
return strings.TrimSpace(string(pw[0:n])), nil
}
func sendInviteEmail(invite invites.Invite) error {

View File

@ -153,8 +153,6 @@ func _main() error {
r := newReviewer(signupDB, u.Username)
rand.Seed(time.Now().Unix())
su := models.TownSignup{}
signups, err := su.All(signupDB)
@ -437,7 +435,8 @@ func _main() error {
if exiterr, ok := err.(*exec.ExitError); ok {
// no match or interrupt. who cares
switch exiterr.ExitCode() {
case 1: case 130:
case 1:
case 130:
return
}
}

101
cmd/tma/main.go 100644
View File

@ -0,0 +1,101 @@
package main
import (
"errors"
"fmt"
"os"
"strconv"
"time"
"database/sql"
"git.tilde.town/tildetown/town/towndb"
_ "github.com/mattn/go-sqlite3"
)
type dbs struct {
Invites *sql.DB
Signups *sql.DB
Users *sql.DB
}
func connect() (*dbs, error) {
users, err := sql.Open("sqlite3", "/town/var/town.db?mode=r")
if err != nil {
return nil, err
}
signups, err := sql.Open("sqlite3", "/town/var/signups/signups.db?mode=r")
if err != nil {
return nil, err
}
invites, err := sql.Open("sqlite3", "/town/var/invites/invites.db?mode=r")
if err != nil {
return nil, err
}
return &dbs{
Invites: invites,
Signups: signups,
Users: users,
}, nil
}
func _main(argv []string) error {
if len(argv) < 3 {
return errors.New("want two args (user|email) <lookup>")
}
switch argv[1] {
case "user":
return userLookup(argv[2])
case "email":
return emailLookup(argv[2])
default:
return fmt.Errorf("idk %s", argv[2])
}
}
func userLookup(username string) error {
dbs, err := connect()
if err != nil {
return err
}
stmt, err := dbs.Users.Prepare("SELECT id,created,username,state,admin FROM users WHERE username = ?")
if err != nil {
return fmt.Errorf("user select prepare failed: %w", err)
}
var created string
tu := towndb.TownUser{}
err = stmt.QueryRow(username).Scan(&tu.ID, &created, &tu.Username, &tu.State, &tu.IsAdmin)
if err != nil {
return fmt.Errorf("failed to select user: %w", err)
}
i, err := strconv.ParseInt(created, 10, 64)
if err != nil {
return fmt.Errorf("time what? %w", err)
}
tu.Created = time.Unix(i, 0)
fmt.Println("id,created,username,state,admin")
fmt.Printf("%d,%s,%s,%s,%v\n", tu.ID, tu.Created, tu.Username, tu.State, tu.IsAdmin)
// TODO rest
return nil
}
func emailLookup(address string) error {
// TODO
return nil
}
func main() {
if err := _main(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
os.Exit(1)
}
}

View File

View File

@ -0,0 +1,26 @@
.-'''-.
' _ \
/ /` '. \ _..._
. | \ ' _ _ .' '.
.| | ' | '/\ \\ //. .-. .
.' |_\ \ / / `\\ //\\ // | ' ' |
.' |`. ` ..' / \`// \'/ | | | |
'--. .-' '-...-'` \| |/ | | | |
| | ' | | | |
| | | | | |
| '.' | | | |
| / | | | |
`'-' '--' '--'
_..._ .-'''-.
.-'_..._''. ' _ \
.' .' '.\/ /` '. \ _..._
/ .' . | \ ' .' '.
. ' | ' | '. .-. .
| | \ \ / / | ' ' |
| | `. ` ..' / | | | |
. ' '-...-'` | | | |
\ '. . | | | |
'. `._____.-'/ | | | |
`-.______ / | | | |
` | | | |
'--' '--'

View File

@ -0,0 +1,32 @@
,----,
,/ .`| ,----.. ,--.
,` .' : / / \ .---. ,--.'|
; ; // . : /. ./| ,--,: : |
.'___,/ ,'. / ;. \ .--'. ' ;,`--.'`| ' :
| : |. ; / ` ; /__./ \ : || : : | |
; |.'; ;; | ; \ ; | .--'. ' \' .: | \ | :
`----' | || : | ; | '/___/ \ | ' '| : ' '; |
' : ;. | ' ' ' :; \ \; :' ' ;. ;
| | '' ; \; / | \ ; ` || | | \ |
' : | \ \ ', / . \ .\ ;' : | ; .'
; |.' ; : / \ \ ' \ || | '`--'
'---' \ \ .' : ' |--" ' : |
`---` \ \ ; ; |.'
'---" '---'
,----.. ,--.
,----.. / / \ ,--.'|
/ / \ / . : ,--,: : |
| : : . / ;. \,`--.'`| ' :
. | ;. /. ; / ` ;| : : | |
. ; /--` ; | ; \ ; |: | \ | :
; | ; | : | ; | '| : ' '; |
| : | . | ' ' ' :' ' ;. ;
. | '___ ' ; \; / || | | \ |
' ; : .'| \ \ ', / ' : | ; .'
' | '/ : ; : / | | '`--'
| : / \ \ .' ' : |
\ \ .' `---` ; |.'
`---` '---'

View File

@ -0,0 +1,18 @@
__/\\\\\\\\\\\\\\\_______/\\\\\_______/\\\______________/\\\__/\\\\\_____/\\\_
_\///////\\\/////______/\\\///\\\____\/\\\_____________\/\\\_\/\\\\\\___\/\\\_
_______\/\\\_________/\\\/__\///\\\__\/\\\_____________\/\\\_\/\\\/\\\__\/\\\_
_______\/\\\________/\\\______\//\\\_\//\\\____/\\\____/\\\__\/\\\//\\\_\/\\\_
_______\/\\\_______\/\\\_______\/\\\__\//\\\__/\\\\\__/\\\___\/\\\\//\\\\/\\\_
_______\/\\\_______\//\\\______/\\\____\//\\\/\\\/\\\/\\\____\/\\\_\//\\\/\\\_
_______\/\\\________\///\\\__/\\\_______\//\\\\\\//\\\\\_____\/\\\__\//\\\\\\_
_______\/\\\__________\///\\\\\/_________\//\\\__\//\\\______\/\\\___\//\\\\\_
_______\///_____________\/////____________\///____\///_______\///_____\/////__
__________________/\\\\\\\\\_______/\\\\\_______/\\\\\_____/\\\_
_______________/\\\////////______/\\\///\\\____\/\\\\\\___\/\\\_
_____________/\\\/_____________/\\\/__\///\\\__\/\\\/\\\__\/\\\_
____________/\\\______________/\\\______\//\\\_\/\\\//\\\_\/\\\_
___________\/\\\_____________\/\\\_______\/\\\_\/\\\\//\\\\/\\\_
___________\//\\\____________\//\\\______/\\\__\/\\\_\//\\\/\\\_
____________\///\\\___________\///\\\__/\\\____\/\\\__\//\\\\\\_
______________\////\\\\\\\\\____\///\\\\\/_____\/\\\___\//\\\\\_
_________________\/////////_______\/////_______\///_____\/////__

View File

@ -0,0 +1,10 @@
d8P
d888888P
?88' d8888b ?88 d8P d8P 88bd88b d8888b d8888b 88bd88b
88P d8P' ?88 d88 d8P' d8P' 88P' ?8b d8P' `Pd8P' ?88 88P' ?8b
88b 88b d88 ?8b ,88b ,88' d88 88P 88b 88b d88 d88 88P
`?8b `?8888P' `?888P'888P' d88' 88b `?888P'`?8888P'd88' 88b

216
cmd/towncon/main.go 100644
View File

@ -0,0 +1,216 @@
package main
import (
"embed"
"fmt"
"math/rand"
"os"
"os/exec"
"os/user"
"path"
"strings"
"git.tilde.town/tildetown/town/email"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/huh"
"gopkg.in/yaml.v3"
)
//go:embed banners/*
var banners embed.FS
//go:embed md/*
var md embed.FS
func banner() (string, error) {
dirs, err := banners.ReadDir("banners")
if err != nil {
return "", err
}
mx := len(dirs)
ix := rand.Intn(mx)
content, err := banners.ReadFile(path.Join("banners", dirs[ix].Name()))
if err != nil {
return "", err
}
return string(content), nil
}
func info() error {
infoContent, err := md.ReadFile("md/info.md")
if err != nil {
return err
}
out, err := glamour.Render(string(infoContent), "dracula")
if err != nil {
return err
}
cmd := exec.Command("/usr/bin/bat")
cmd.Stdin = strings.NewReader(out)
cmd.Stdout = os.Stdout
return cmd.Run()
}
type physicalData struct {
Arriving string
Departing string
Transportation bool
Lodging bool
Allergies string
Couch bool
}
type rsvpData struct {
Being string
PhysicalData *physicalData `yaml:"PhysicalData,omitempty"`
Freeform string
}
func sendRSVPEmail(rd rsvpData) error {
bs, err := yaml.Marshal(rd)
if err != nil {
return err
}
return email.SendLocalEmail("vilmibm", "RSVP TOWNCON24", string(bs))
}
func rsvp(o opts) error {
var being string
var arriving string
var departing string
var transportation bool
lodging := true
var allergies string
couch := true
var freeform string
form := huh.NewForm(
huh.NewGroup(
huh.NewSelect[string]().Title("How will you be attending?").
Options(
huh.NewOption("Digitally", "digital"),
huh.NewOption("Phyiscally", "physical"),
).Value(&being)),
// Physical attendee form
huh.NewGroup(
huh.NewInput().Title("When (day/time) are you arriving?").
Value(&arriving),
huh.NewInput().Title("When (day/time) are you departing?").
Value(&departing),
).WithHideFunc(func() bool {
return being != "physical"
}),
huh.NewGroup(
huh.NewConfirm().Title("Will you be staying overnight at the venue?").
Value(&lodging),
).WithHideFunc(func() bool {
return being != "physical"
}),
huh.NewGroup(
huh.NewConfirm().Title("If the need arises are you ok sleeping on a couch?").
Value(&couch),
).WithHideFunc(func() bool {
return being != "physical" && !lodging
}),
huh.NewGroup(
huh.NewInput().Title("Do you have any food allergies I should be aware of?").
Value(&allergies),
huh.NewConfirm().Title("Will you need any help getting to the venue?").
Description("I have a car and have some ability to help getting people to the venue").
Value(&transportation),
).WithHideFunc(func() bool {
return being != "physical"
}),
// Catch all freeform
huh.NewGroup(
huh.NewText().
Title("Anything you want me to know? Any questions?").
Value(&freeform)))
err := form.Run()
if err != nil {
return err
}
var pd *physicalData
if being == "physical" {
pd = &physicalData{
Arriving: arriving,
Departing: departing,
Transportation: transportation,
Lodging: lodging,
Allergies: allergies,
Couch: couch,
}
}
rd := rsvpData{
Being: being,
Freeform: freeform,
PhysicalData: pd}
return sendRSVPEmail(rd)
}
type opts struct {
Username string
}
func _main() error {
b, err := banner()
if err != nil {
return err
}
fmt.Println(b)
u, err := user.Current()
if err != nil {
return err
}
o := opts{
Username: u.Username,
}
fmt.Printf("\t\t^_^_^_^_^_^_^ hi ~%s ^_^_^_^_^_^_^\n", o.Username)
fmt.Println()
var mode string
huh.NewSelect[string]().Title("whuddyu wanna doo?").
Options(
huh.NewOption("Get an info dump", "info"),
huh.NewOption("RSVP", "rsvp"),
huh.NewOption("Submit a talk proposal or finished work", "submit"),
huh.NewOption("Propose a creative jam", "jam"),
huh.NewOption("Quit", "quit"),
).Value(&mode).Run()
switch mode {
case "info":
return info()
case "rsvp":
return rsvp(o)
case "submit":
case "jam":
case "quit":
default:
return fmt.Errorf("wtf '%s'", mode)
}
return nil
}
func main() {
if err := _main(); err != nil {
fmt.Fprintf(os.Stderr, ":( failed: %s\n", err.Error())
os.Exit(1)
}
}

View File

@ -0,0 +1,96 @@
# TOWN CON INFORMATION
TOWN CON is a celebration of tilde.town's 10 year anniversary featuring 24 hours of talks, art, and music by townies.
TOWN CON begins at 0:00 UTC on October 11, 2024 and ends 23:59 UTC on October 12, 2024.
There is an additional day of TOWN CON on October 12th for townies attending TOWN CON MEAT EDITION in Chicago, Illinois, USA.
## DIGITAL EVENT
TOWN CON's primary locus of reality will be here on the server in a text-mode environment. This environment will be supplemented with audio/video feeds as needed.
Works submitted by townies will all be slotted into a viewing schedule to run throughout the 24 hours of the event. The text-mode environment will either show the submitted work or have a pointer off to where it can be consumed in a browser.
The text mode environment will either be a HERMETICUM instance (if I finish the project in time) or just IRC.
### JAMS
Throughout the event townies are encouraged to cluster and collaborate on various creative **JAMS**. Re-run this tool and select _Propose a creative jam_ if you want to suggest one. Suggested jams:
- 88x31 badge jam
- forum software jam
- game jam
- dos jam
## PHYSICAL EVENT
TOWN CON MEAT EDITION takes place October 11th and 12th in Chicago, Illinois, USA.
On the 11th we'll be plugged into the digital event together and on the 12th we'll do something in meatspace.
### live music jam
Meat edition guests are encouraged to bring small sound or music emitting devices. We'll set them up with recording system and do some jamming. Some synthesizers and a sampler will be provided.
### VENUE and LODGING
The venue is a converted warehouse loft located at:
```
Loft 606
2605 W Armitage Ave,
Chicago, IL 60647
```
The venue has enough sleeping space to lodge everyone who expressed interest in coming in person, however some people will need to volunteer to sleep on couches. I have the venue reserved for:
- the evening of October 10th (feel free to arrive this day!)
- all day October 11th
- all day October 12th
- the morning of October 13th (feel free to leave this day!)
The total cost to book this venue was **$8,637**. I've paid for it out of pocket and would love if townies could help chip in for the cost. As of right now (`2024-08-31`) I have received **$0** of chip in and will update this as I get donations.
To learn more about the venue you can visit their website: https://www.loft606.com/
### FOOD
I'll arrange food throughout the event.
- 10th
- **dinner**: local restaurant food for dinner
- 11th
- breakfast: bulk order from Colectivo
- lunch: sandwich fixins
- dinner: vegan deep dish pizza from Kitchen 17
- 12th
- breakfast: bulk order from Colectivo
- lunch: sandwich fixins
- dinner: vegan mexican food from Quesadilla Del Reina Del Sur
- 13th
- breakfast: bulk order from Colectivo
There are also plenty of restaurants around the venue if people want to get their own food.
### TRANSPORTATION
The venue is accessible from rail, bus, and cab/uber/lyft. I have a car and can make myself available to pick people up and get them to the venue once they have made it to Chicago if they need. Depending on timing I might not be able to help, however. Contact via my cell phone (`~vilmibm/phone.txt`) to ask about my availability for a ride during the event.
If you are driving parking might be tough in the area--I can offer a few spots by my place in Forest Park if you want to put a car there and then get a ride over to the venue.
### ACTIVITIES
I'd like to do a field trip on the 12th...Some potential ideas:
- go to the nearby thrift store
- go out to minigolf
- go to a museum
- go into the woods
- just hang out and play synths (field trip of the mind)
!!! TBD !!!
## COMMS
For any kind of emergency during the event please contact me via my cell phone. My number can be found at `~vilmibm/phone.txt`.

View File

@ -3,13 +3,11 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/glamour"
@ -39,7 +37,6 @@ func main() {
}
func visitRandomUser() error {
rand.Seed(time.Now().UnixNano())
usernames, err := getUsernames()
if err != nil {
return err
@ -78,7 +75,7 @@ func visitPrompt() error {
}
func visitUser(username string) error {
files, err := ioutil.ReadDir(filepath.Join("/home", username))
files, err := os.ReadDir(filepath.Join("/home", username))
if err != nil {
return fmt.Errorf("user is not accepting visitors (could not read user's home directory: %w)", err)
}
@ -90,7 +87,7 @@ func visitUser(username string) error {
path := filepath.Join("/home", username, file.Name())
data, err := ioutil.ReadFile(path)
data, err := os.ReadFile(path)
if err != nil {
break
}

47
codes/codes.go 100644
View File

@ -0,0 +1,47 @@
package codes
import (
"crypto/rand"
"encoding/base64"
"math/big"
"strings"
)
const codeLen = 32
func NewCode(email string) string {
charset := "abcdefghijklmnopqrztuvwxyz"
charset += strings.ToUpper(charset)
charset += "0123456789"
charset += "`~!@#$%^&*()-=_+[]{}|;:,./<>?"
code := []byte{}
max := big.NewInt(int64(len(charset)))
for len(code) < codeLen {
ix, err := rand.Int(rand.Reader, max)
if err != nil {
// TODO this is bad but I'm just kind of hoping it doesn't happen...often
panic(err)
}
code = append(code, charset[ix.Int64()])
}
code = append(code, ' ')
eb := []byte(email)
for x := 0; x < len(eb); x++ {
code = append(code, eb[x])
}
return base64.StdEncoding.EncodeToString(code)
}
func Decode(code string) ([]string, error) {
decoded, err := base64.StdEncoding.DecodeString(code)
if err != nil {
return nil, err
}
return strings.Split(string(decoded), " "), nil
}

View File

@ -4,13 +4,15 @@ import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/mail"
"net/smtp"
"os/exec"
)
const (
from = "root@tilde.town"
SMTPHost = "smtp.zoho.com"
SMTPHost = "smtp.migadu.com"
SMTPPort = 465
)
@ -38,61 +40,79 @@ func NewExternalMailer(pw string) *ExternalMailer {
}
}
func (m *ExternalMailer) Send(address, subject, body string) error {
headers := map[string]string{
"From": from,
"To": address,
"Subject": subject,
}
func (m *ExternalMailer) Send(address, subj, body string) error {
from := mail.Address{Name: "Tilde Town Admins", Address: "root@tilde.town"}
to := mail.Address{Name: "", Address: address}
// Setup headers
headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = subj
// Setup message
message := ""
for k, v := range headers {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + body
auth := smtp.PlainAuth("", from, m.Password, SMTPHost)
// Connect to the SMTP Server
servername := fmt.Sprintf("%s:%d", SMTPHost, SMTPPort)
server := fmt.Sprintf("%s:%d", SMTPHost, SMTPPort)
host, _, _ := net.SplitHostPort(servername)
tlsconf := &tls.Config{
auth := smtp.PlainAuth("", "root@tilde.town", m.Password, host)
// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: server,
ServerName: host,
}
conn, err := tls.Dial("tcp", server, tlsconf)
// Here is the key, you need to call tls.Dial instead of smtp.Dial
// for smtp servers running on 465 that require an ssl connection
// from the very beginning (no starttls)
conn, err := tls.Dial("tcp", servername, tlsconfig)
if err != nil {
return err
return fmt.Errorf("failed dial: %w", err)
}
c, err := smtp.NewClient(conn, SMTPHost)
c, err := smtp.NewClient(conn, host)
if err != nil {
return err
return fmt.Errorf("failed to make smtp client: %w", err)
}
// Auth
if err = c.Auth(auth); err != nil {
return fmt.Errorf("auth failed for smtp: %w", err)
return fmt.Errorf("failed to make auth: %w", err)
}
if err = c.Mail(from); err != nil {
return err
// To && From
if err = c.Mail(from.Address); err != nil {
return fmt.Errorf("failed to create mail: %w", err)
}
if err = c.Rcpt(address); err != nil {
return err
if err = c.Rcpt(to.Address); err != nil {
return fmt.Errorf("failed to add rcpt: %w", err)
}
// Data
w, err := c.Data()
if err != nil {
return err
return fmt.Errorf("failed to send data: %w", err)
}
_, err = w.Write([]byte(message))
if err != nil {
return err
return fmt.Errorf("failed to write: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("failed to close: %w", err)
}
w.Close()
c.Quit()
return nil

1
external/README.md vendored 100644
View File

@ -0,0 +1 @@
this folder contains the things that external people can access via ssh: join@tilde.town, welcome@tilde.town, and help@tilde.town

61
external/cmd/help/email.go vendored 100644
View File

@ -0,0 +1,61 @@
package main
import (
"errors"
"fmt"
"os"
"strings"
"git.tilde.town/tildetown/town/email"
)
const emailText = `hello!
You (hopefully) requested to add a new public key to your tilde.town account.
If you didn't, feel free to ignore this email (or report it to an admin).
If you did, here is your auth code: %s
To use this code, please open a terminal and run:
ssh help@tilde.town
Follow the instructions there to add your new key and restore access to your account.
best,
~vilmibm`
func loadPassword() (string, error) {
f, err := os.Open("/town/docs/smtp_help.pw")
if err != nil {
return "", fmt.Errorf("could not open smtp password file: %w", err)
}
pw := make([]byte, 100)
n, err := f.Read(pw)
if err != nil {
return "", fmt.Errorf("could not read smtp password file: %w", err)
}
if n == 0 {
return "", errors.New("smtp password file was empty")
}
return strings.TrimSpace(string(pw[0:n])), nil
}
func sendAuthCodeEmail(ac AuthCode) error {
pw, err := loadPassword()
if err != nil {
return err
}
body := fmt.Sprintf(emailText, ac.Code)
mailer := email.NewExternalMailer(pw)
return mailer.Send(
ac.Email,
"Adding a new tilde.town public key",
body)
}

404
external/cmd/help/main.go vendored 100644
View File

@ -0,0 +1,404 @@
package main
import (
"bytes"
"database/sql"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"git.tilde.town/tildetown/town/codes"
"git.tilde.town/tildetown/town/external/lockingwriter"
"git.tilde.town/tildetown/town/sshkey"
"github.com/charmbracelet/lipgloss"
_ "github.com/mattn/go-sqlite3"
"github.com/mattn/go-tty"
)
// TODO consider local-only help command for renaming, email mgmt, deleting account
// TODO put colorscheme, prompting stuff into own packages for use in the other commands. would be good to get off of survey.
// TODO use new lockingwriter for logging in the other external commands
func connectDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "/town/var/codes/codes.db?mode=rw")
if err != nil {
return nil, err
}
return db, nil
}
type colorScheme struct {
Header func(...string) string
Subtitle func(...string) string
Prompt func(...string) string
Email func(...string) string
Option func(...string) string
Error func(...string) string
}
func newColorScheme() colorScheme {
s2r := func(s lipgloss.Style) func(...string) string {
return s.Render
}
c := func(s string) lipgloss.Color {
return lipgloss.Color(s)
}
s := lipgloss.NewStyle
return colorScheme{
Header: s2r(s().Bold(true).Foreground(c("#E0B0FF"))),
Subtitle: s2r(s().Italic(true).Foreground(c("gray"))),
Email: s2r(s().Bold(true).Underline(true)),
Prompt: s2r(s().Bold(true).Foreground(c("#00752d"))),
Option: s2r(s().Bold(true).Foreground(c("#38747a"))),
Error: s2r(s().Bold(true).Foreground(c("#f43124"))),
}
}
type Prompter struct {
cs colorScheme
tty *tty.TTY
}
func NewPrompter(tty *tty.TTY, cs colorScheme) *Prompter {
return &Prompter{
cs: cs,
tty: tty,
}
}
func (p *Prompter) String(prompt string) (string, error) {
// TODO assumes blank is no bueno
var err error
var answer string
for answer == "" {
fmt.Println("")
fmt.Println(p.cs.Prompt(prompt))
fmt.Println(p.cs.Subtitle("(press enter to submit)"))
answer, err = p.tty.ReadString()
if err != nil {
return "", fmt.Errorf("couldn't collect input: %w", err)
}
}
return answer, nil
}
func (p *Prompter) Select(prompt string, opts []string) (int, error) {
fmt.Println()
fmt.Println(p.cs.Prompt(prompt))
fmt.Println(p.cs.Subtitle("(pick an option using the corresponding number)"))
chosen := -1
for chosen < 0 {
fmt.Println()
for ix, o := range opts {
fmt.Printf("%s: %s\n", p.cs.Option(fmt.Sprintf("%d", ix+1)), o)
}
r, err := p.tty.ReadRune()
if err != nil {
return -1, fmt.Errorf("could not collect answer for '%s': %w", prompt, err)
}
c, err := strconv.Atoi(string(r))
if err != nil {
fmt.Println()
fmt.Printf("I could not understand '%s'. Try again, please.\n", string(r))
continue
}
if c > len(opts) || c == 0 {
fmt.Println()
fmt.Printf("%s is not an option. Try again, please.\n", string(r))
continue
}
chosen = c - 1
}
fmt.Println("")
return chosen, nil
}
func _main(cs colorScheme) error {
lw := lockingwriter.New()
l := log.New(lw, "help: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile|log.Lmsgprefix)
db, err := connectDB()
if err != nil {
return fmt.Errorf("could not connect to database. please let root@tilde.town know about this.")
}
fmt.Println(cs.Header("Hi, you have reached the tilde town help desk."))
fmt.Println()
fmt.Println("Please check out the options below.")
fmt.Printf("If none of them apply to your case, you can email %s. \n", cs.Email("root@tilde.town"))
tty, err := tty.Open()
if err != nil {
return fmt.Errorf("could not open tty: %w", err)
}
defer tty.Close()
p := NewPrompter(tty, cs)
options := []string{
"I need to request that a new SSH key be added to my account.",
"I have a code from my e-mail to redeem for a new SSH key",
"I just want out of here",
}
c, err := p.Select("What do you need help with?", options)
defer func() {
}()
switch c {
case 0:
return collectEmail(l, db, cs, p)
case 1:
return redeemCode(l, db, cs, p)
case 2:
return nil
}
return nil
}
func emailToUsername(email string) (string, error) {
cmd := exec.Command("sudo", "/town/bin/emailtouser", email)
stderrBuff := bytes.NewBuffer([]byte{})
stdoutBuff := bytes.NewBuffer([]byte{})
cmd.Stderr = stderrBuff
cmd.Stdout = stdoutBuff
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("emailtouser failed with '%s': %w", stderrBuff.String(), err)
}
return strings.TrimSpace(stdoutBuff.String()), nil
}
func collectEmail(l *log.Logger, db *sql.DB, cs colorScheme, p *Prompter) error {
fmt.Println(cs.Header("We can send a authorization code to an email associated with your town account."))
email, err := p.String("email to send reset code to?")
if err != nil {
return err
}
fmt.Println()
fmt.Println(cs.Header("thanks!"))
fmt.Println()
fmt.Printf("If %s is associated with a town account we'll email an authorization code.\n", cs.Email(email))
mustHave := []string{"@", "."}
found := 0
for _, s := range mustHave {
if strings.Contains(email, s) {
found++
}
}
if found != len(mustHave) {
l.Printf("corrupt email '%s'", email)
return nil
}
if _, err = emailToUsername(email); err != nil {
l.Printf("no user for '%s'", email)
return nil
}
code := codes.NewCode(email)
ac := &AuthCode{
Code: code,
Email: email,
}
if err = ac.Insert(db); err != nil {
l.Printf("database error: %s", err.Error())
return errors.New("the database was sad")
}
if err = sendAuthCodeEmail(*ac); err != nil {
l.Printf("mail send error: %s", err.Error())
return errors.New("email sending failed")
}
return nil
}
func redeemCode(l *log.Logger, db *sql.DB, cs colorScheme, p *Prompter) error {
fmt.Println(cs.Header("redeem an auth code and add a new public key"))
c, err := p.String("paste your auth code:")
if err != nil {
l.Printf("failed to prompt: %s", err.Error())
fmt.Println(cs.Error("sorry, I couldn't read that."))
return nil
}
parts, err := codes.Decode(c)
if err != nil {
l.Printf("failed to decode auth code: %s", err.Error())
fmt.Println(cs.Error("sorry, that doesn't look like an auth code..."))
return nil
}
code := &AuthCode{
Code: c,
Email: parts[1],
}
err = code.Hydrate(db)
if err != nil {
l.Printf("hydrate failed: %s", err.Error())
return errors.New("the database is sad")
}
if code.Used {
fmt.Println(cs.Error("That code has already been redeemed. You'll have to request a new one."))
return nil
}
username, err := emailToUsername(code.Email)
if err != nil {
l.Printf("could not find user: %s", err.Error())
fmt.Println(cs.Error("That code doesn't seem to match an account."))
return nil
}
fmt.Println()
fmt.Printf("hi, ~%s", username)
key, err := p.String("paste your new public key:")
if err != nil {
l.Printf("failed to prompt: %s", err.Error())
fmt.Println(cs.Error("sorry, I couldn't read that."))
return nil
}
valid, err := sshkey.ValidKey(key)
if err != nil {
return fmt.Errorf("failed to validate key: %w", err)
}
if !valid {
errMsg := fmt.Sprintf("that key is invalid: %s", err.Error())
fmt.Println(cs.Error(errMsg))
return nil
}
cmd := exec.Command("sudo", "--user", username, "/town/bin/appendkeyfile")
cmd.Stdin = bytes.NewBufferString(key + "\n")
stdoutBuff := bytes.NewBuffer([]byte{})
cmd.Stdout = stdoutBuff
stderrBuff := bytes.NewBuffer([]byte{})
cmd.Stderr = stderrBuff
if err = cmd.Run(); err != nil {
l.Printf("appendkeyfile failed with '%s', '%s': %s", stderrBuff.String(), stdoutBuff.String(), err.Error())
return errors.New("adding to keys file failed")
}
err = code.MarkUsed(db)
if err != nil {
l.Printf("failed to mark used: %s", err.Error())
return errors.New("database was sad")
}
fmt.Println()
fmt.Println("new key added! you should be able to use it to log in.")
return nil
}
func main() {
cs := newColorScheme()
err := _main(cs)
defer func() {
fmt.Println()
fmt.Println(cs.Header("bye~"))
}()
if err != nil {
fmt.Println(
cs.Error(fmt.Sprintf("sorry, something went wrong: %s", err.Error())))
fmt.Println("Please let an admin know by emailing a copy of this error to root@tilde.town")
os.Exit(1)
}
}
type AuthCode struct {
ID int64
Code string
Email string
Used bool
}
func (c *AuthCode) Insert(db *sql.DB) error {
stmt, err := db.Prepare(`
INSERT INTO auth_codes (code, email)
VALUES (?, ?)`)
if err != nil {
return err
}
defer stmt.Close()
result, err := stmt.Exec(c.Code, c.Email)
if err != nil {
return err
}
liid, err := result.LastInsertId()
if err != nil {
return err
}
c.ID = liid
return nil
}
func (c *AuthCode) Hydrate(db *sql.DB) error {
stmt, err := db.Prepare(`
SELECT id, used
FROM auth_codes
WHERE code = ? AND email = ?`)
if err != nil {
return err
}
defer stmt.Close()
return stmt.QueryRow(c.Code, c.Email).Scan(&c.ID, &c.Used)
}
func (c *AuthCode) MarkUsed(db *sql.DB) error {
if c.ID == 0 {
return errors.New("not hydrated")
}
stmt, err := db.Prepare(`
UPDATE auth_codes SET used = 1 WHERE id = ?`)
if err != nil {
return err
}
defer stmt.Close()
result, err := stmt.Exec(c.ID)
if err != nil {
return err
}
var rowsAffected int64
if rowsAffected, err = result.RowsAffected(); err != nil {
return err
}
if rowsAffected == 0 {
return errors.New("no rows affected")
}
return nil
}

View File

@ -0,0 +1,59 @@
package main
/*
The purpose of this command is to be run via sudo as an arbitrary user by the "help" user. It is invoked as part of the "i need to add a new public key" flow from "ssh help@tilde.town".
It's based on the createkeyfile helper and heavily copy pasta'd. They should probably share code or be a single command but I wanted to keep things simple for now.
*/
import (
"fmt"
"os"
"os/user"
"path"
)
const keyfileName = "authorized_keys2"
func quit(msg string, code int) {
// TODO print to stderr
fmt.Println(msg)
os.Exit(code)
}
func main() {
u, err := user.Current()
if err != nil {
quit(err.Error(), 2)
}
sshPath := path.Join("/home", u.Username, ".ssh")
keyfilePath := path.Join(sshPath, keyfileName)
f, err := os.OpenFile(keyfilePath, os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
quit(fmt.Sprintf("failed to open %s: %s", keyfilePath, err.Error()), 5)
}
defer f.Close()
stdin := make([]byte, 90000) // arbitrary limit
n, err := os.Stdin.Read(stdin)
if err != nil {
quit(err.Error(), 6)
} else if n == 0 {
quit("nothing passed on STDIN", 7)
}
stdin = stdin[0:n]
n, err = f.Write(stdin)
if err != nil {
quit(err.Error(), 9)
} else if n == 0 {
quit("wrote nothing to keyfile", 10)
}
}

View File

@ -76,12 +76,13 @@ func main() {
quit(fmt.Sprintf("file contents look wrong: %s", string(stdin)), 8)
}
n, err = f.Write(stdin)
_, err = f.Write(stdin)
if err != nil {
quit(err.Error(), 9)
} else if n == 0 {
quit("wrote nothing to keyfile", 10)
}
_, err = f.WriteString("\n")
}
/*

View File

@ -0,0 +1,40 @@
package main
import (
"errors"
"fmt"
"os"
"git.tilde.town/tildetown/town/towndb"
)
func _main(args []string) error {
if len(args) < 2 {
return errors.New("need email")
}
email := args[1]
db, err := towndb.ConnectDB()
if err != nil {
return err
}
user, err := towndb.UserForEmail(db, email)
if err != nil {
return err
}
if user == nil {
return errors.New("email does not correspond to user")
}
fmt.Print(user.Username)
return nil
}
func main() {
if err := _main(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}

View File

@ -134,7 +134,6 @@ func main() {
}
func _main(l *log.Logger, db *sql.DB) error {
l.Println("starting a session")
pages := tview.NewPages()
mainFlex := tview.NewFlex()
input := tview.NewTextArea()
@ -305,7 +304,6 @@ func _main(l *log.Logger, db *sql.DB) error {
/nod
and pressing enter will cause you to nod. some other verbs: /quit /look`))
case "quit":
l.Println("got /quit")
app.Stop()
case "look":
fmt.Fprintln(msgScroll, "")
@ -314,8 +312,6 @@ func _main(l *log.Logger, db *sql.DB) error {
if !sm.Advance() {
fmt.Fprintln(msgScroll, "you nod, but nothing happens.")
fmt.Fprintln(msgScroll)
} else {
l.Println("advancing scene")
}
}
return
@ -334,10 +330,7 @@ func _main(l *log.Logger, db *sql.DB) error {
msgScroll.ScrollToEnd()
}
defer func() {
l.Println("exiting")
db.Close()
}()
defer db.Close()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {

View File

@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"git.tilde.town/tildetown/town/invites"
@ -142,7 +143,7 @@ This program is going to exit and you are now free to ssh to town as yourself:
if your public key isn't found by ssh, you'll need to explain to ssh how to find it with:
ssh -i "replace with path to public key file" %[1]s@tilde.town
ssh -i "replace with path to private key file" %[1]s@tilde.town
for help with ssh, see: https://tilde.town/wiki/getting-started/ssh.html
@ -205,12 +206,16 @@ func createUser(data newUserData) (err error) {
}
func keyfileText(data newUserData) string {
pkey := data.PubKey
if !strings.HasSuffix(pkey, "\n") {
pkey += "\n"
}
header := `########## GREETINGS! ##########
# Hi! This file was automatically generated by tilde.town when
# This file was automatically generated by tilde.town when
# your account was created. You can edit it if you want, but we
# recommend adding stuff to ~/.ssh/authorized_keys instead.`
return fmt.Sprintf("%s\n%s", header, data.PubKey)
return fmt.Sprintf("%s\n%s", header, pkey)
}
func main() {

View File

@ -0,0 +1,64 @@
package lockingwriter
import (
"os"
"time"
"github.com/gofrs/flock"
)
// for now this package defines a writer for use with log.New(). It it intended to be used from the external ssh applications. This logger uses a file lock to allow all the various ssh applications to log to the same file.
// the correct way to do this is to send log events to a daemon
// but i'm trying to cut a corner
type LockingWriter struct {
path string
}
const (
fp = "/town/var/log/external.log"
lp = "/town/var/log/log.lock"
)
func New() *LockingWriter {
f, err := os.OpenFile(fp, os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0660)
if err != nil && !os.IsExist(err) {
panic(err)
}
f.Close()
return &LockingWriter{
path: fp,
}
}
func (l *LockingWriter) Write(p []byte) (n int, err error) {
fl := flock.New(lp)
var locked bool
for !locked {
locked, err = fl.TryLock()
if err != nil {
return
}
time.Sleep(time.Second)
}
var f *os.File
f, err = os.OpenFile(l.path, os.O_APPEND|os.O_WRONLY, 0660)
if err != nil {
return
}
defer f.Close()
defer fl.Unlock()
n, err = f.Write(p)
if err != nil {
panic(err)
}
return
//return f.Write(p)
}

34
go.mod
View File

@ -6,9 +6,12 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.5
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/charmbracelet/glamour v0.5.0
github.com/charmbracelet/lipgloss v0.6.0
github.com/charmbracelet/huh v0.5.3
github.com/charmbracelet/lipgloss v0.13.0
github.com/gdamore/tcell/v2 v2.5.3
github.com/gofrs/flock v0.8.1
github.com/mattn/go-sqlite3 v1.14.16
github.com/mattn/go-tty v0.0.5
github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
@ -16,27 +19,42 @@ require (
require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.19.0 // indirect
github.com/charmbracelet/bubbletea v0.27.0 // indirect
github.com/charmbracelet/x/ansi v0.2.2 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/gdamore/encoding v1.0.0 // 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
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microcosm-cc/bluemonday v1.0.17 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/rivo/uniseg v0.4.7 // 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/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.16.0 // indirect
)

74
go.sum
View File

@ -1,17 +1,36 @@
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=
github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
github.com/charmbracelet/bubbletea v0.27.0 h1:Mznj+vvYuYagD9Pn2mY7fuelGvP0HAXtZYGgRBCbHvU=
github.com/charmbracelet/bubbletea v0.27.0/go.mod h1:5MdP9XH6MbQkgGhnlxUqCNmBXf9I74KRQ8HIidRxV1Y=
github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g=
github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc=
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
github.com/charmbracelet/huh v0.5.3 h1:3KLP4a/K1/S4dq4xFMTNMt3XWhgMl/yx8NYtygQ0bmg=
github.com/charmbracelet/huh v0.5.3/go.mod h1:OZC3lshuF+/y8laj//DoZdFSHxC51OrtXLJI8xWVouQ=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
github.com/charmbracelet/x/ansi v0.2.2 h1:BC7xzaVpfWIYZRNE8NhO9zo8KA4eGUL6L/JWXDh3GF0=
github.com/charmbracelet/x/ansi v0.2.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -20,10 +39,16 @@ 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
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=
@ -34,29 +59,41 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4=
github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 h1:STjmj0uFfRryL9fzRA/OupNppeAID6QJYPMavTL7jtY=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -65,8 +102,8 @@ github.com/rivo/tview v0.0.0-20230130130022-4a1b7a76c01c h1:zIYU4PjQJ4BnYryMmpyi
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/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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/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=
@ -83,14 +120,20 @@ github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18W
github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
@ -98,8 +141,9 @@ golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4
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/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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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=

View File

@ -1,20 +1,16 @@
package invites
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"math/big"
"strings"
"time"
"git.tilde.town/tildetown/town/codes"
_ "github.com/mattn/go-sqlite3"
)
const (
dsn = "/town/var/invites/invites.db?mode=rw"
codeLen = 32
dsn = "/town/var/invites/invites.db?mode=rw"
)
type Invite struct {
@ -33,7 +29,7 @@ func (i *Invite) Insert(db *sql.DB) error {
return err
}
i.Code = generateCode(i.Email)
i.Code = codes.NewCode(i.Email)
_, err = stmt.Exec(i.Code, i.Email)
if err != nil {
@ -53,44 +49,6 @@ func ConnectDB() (*sql.DB, error) {
return db, nil
}
func generateCode(email string) string {
charset := "abcdefghijklmnopqrztuvwxyz"
charset += strings.ToUpper(charset)
charset += "0123456789"
charset += "`~!@#$%^&*()-=_+[]{}|;:,./<>?"
code := []byte{}
max := big.NewInt(int64(len(charset)))
for len(code) < codeLen {
ix, err := rand.Int(rand.Reader, max)
if err != nil {
// TODO this is bad but I'm just kind of hoping it doesn't happen...often
panic(err)
}
code = append(code, charset[ix.Int64()])
}
code = append(code, ' ')
eb := []byte(email)
for x := 0; x < len(eb); x++ {
code = append(code, eb[x])
}
return base64.StdEncoding.EncodeToString(code)
}
func Decode(code string) ([]string, error) {
decoded, err := base64.StdEncoding.DecodeString(code)
if err != nil {
return nil, err
}
return strings.Split(string(decoded), " "), nil
}
func Get(db *sql.DB, code string) (*Invite, error) {
inv := &Invite{
Code: code,

View File

@ -12,7 +12,6 @@ import (
"os/user"
"path/filepath"
"syscall"
"time"
email "git.tilde.town/tildetown/town/email"
)
@ -178,7 +177,6 @@ if you did _not_ request this, please let an admin know.
}
func genGiteaPassword() string {
rand.Seed(time.Now().UnixNano())
b := make([]byte, 20)
for i := range b {
b[i] = pwLetters[rand.Intn(len(pwLetters))]

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS auth_codes (
id INTEGER PRIMARY KEY,
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
code TEXT,
email TEXT,
used INTEGER DEFAULT 0
);

View File

@ -2,6 +2,7 @@ package towndb
import (
"database/sql"
"errors"
"time"
_ "github.com/mattn/go-sqlite3"
@ -142,6 +143,29 @@ func (u *TownUser) Insert(db *sql.DB) (err error) {
return tx.Commit()
}
// UserForEmail returns the user associated with an email or nil if no matching user is found
func UserForEmail(db *sql.DB, address string) (*TownUser, error) {
stmt, err := db.Prepare(`
SELECT u.id, u.username FROM users u
JOIN emails e ON e.userid = u.id
WHERE e.address = ?
`)
if err != nil {
return nil, err
}
defer stmt.Close()
row := stmt.QueryRow(address)
u := &TownUser{}
if err = row.Scan(&u.ID, &u.Username); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return u, nil
}
func ConnectDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", dsn)
if err != nil {