Compare commits

..

2 Commits
trunk ... trunk

Author SHA1 Message Date
equa 01b214b29f make sure user is root 2023-07-21 17:34:46 -04:00
equa 955aeed8b5 Port old renameuser script and integrate it with towndb 2023-07-21 16:22:01 -04:00
40 changed files with 279 additions and 1435 deletions

13
.gitignore vendored
View File

@ -1,15 +1,10 @@
*.swp
bin/
cmd/launcher/launcher
cmd/request/request
cmd/contrib/contrib
cmd/visit/visit
cmd/signup/signup
cmd/review/review
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
cmd/welcome/welcome
cmd/createkeyfile/createkeyfile
cmd/registeruser/registeruser

View File

@ -1,71 +0,0 @@
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,6 +3,7 @@ package main
import (
"errors"
"fmt"
"math/rand"
"os"
"os/user"
"path"
@ -70,6 +71,8 @@ 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

@ -76,13 +76,12 @@ func main() {
quit(fmt.Sprintf("file contents look wrong: %s", string(stdin)), 8)
}
_, err = f.Write(stdin)
n, 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")
}
/*

7
cmd/help/main.go 100644
View File

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

124
cmd/rename/main.go 100644
View File

@ -0,0 +1,124 @@
package main
import (
"bufio"
"fmt"
"path"
"os"
"os/exec"
"os/user"
"git.tilde.town/tildetown/town/towndb"
)
func confirmRename() {
fmt.Println("this will kill all of the user's current processes. if you're doing this live you should give them a good contact if things break")
var text string
for text != "y\n" {
fmt.Print("enter 'y' to continue: ")
reader := bufio.NewReader(os.Stdin)
text, _ = reader.ReadString('\n')
}
}
func killUser(name string) (err error) {
cmd := exec.Command("pkill", "-SIGKILL", "-u", name)
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if exiterr.ExitCode() == 1 {
return nil
}
}
return err
}
return nil
}
func usermod(old_name string, new_name string) (err error) {
cmd := exec.Command(
"usermod",
"-l", new_name,
"-m",
"-d", path.Join("/home", new_name),
old_name,
)
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
return err
}
cmd = exec.Command("groupmod", "-n", new_name, old_name)
cmd.Stderr = os.Stderr
if err = cmd.Run(); err != nil {
return err
}
return
}
func renameDb(old_name string, new_name string) (err error) {
db, err := towndb.ConnectDB()
if err != nil {
return err
}
err = towndb.RenameUser(db, old_name, new_name)
if err != nil {
return err
}
return nil
}
func quit(text string) {
fmt.Println(text)
os.Exit(1)
}
func main() {
if len(os.Args) != 3 {
quit("usage: rename username new_username")
}
old_name := os.Args[1]
new_name := os.Args[2]
user, err := user.Current()
if err != nil {
quit(fmt.Sprintf("couldn't get user info: %v", err))
}
if user.Uid != "0" {
quit("must be run as root")
}
confirmRename()
if err := killUser(old_name); err != nil {
quit(fmt.Sprintf("pkill failed: %v", err))
}
fmt.Println("killed old processes")
if err := usermod(old_name, new_name); err != nil {
quit(fmt.Sprintf("unix user rename failed: %v", err))
}
fmt.Println("renamed unix user")
if err := renameDb(old_name, new_name); err != nil {
fmt.Println(fmt.Sprintf("couldn't rename user in /town/var/town.db: %v", err))
fmt.Println("something might be amiss with town.db")
} else {
fmt.Println("renamed user in /town/var/town.db")
}
}

View File

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

View File

@ -153,6 +153,8 @@ func _main() error {
r := newReviewer(signupDB, u.Username)
rand.Seed(time.Now().Unix())
su := models.TownSignup{}
signups, err := su.All(signupDB)
@ -435,8 +437,7 @@ 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
}
}

View File

@ -134,6 +134,7 @@ 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()
@ -304,6 +305,7 @@ 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, "")
@ -312,6 +314,8 @@ 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
@ -330,7 +334,10 @@ func _main(l *log.Logger, db *sql.DB) error {
msgScroll.ScrollToEnd()
}
defer db.Close()
defer func() {
l.Println("exiting")
db.Close()
}()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {

View File

@ -1,101 +0,0 @@
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

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
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

View File

@ -1,216 +0,0 @@
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

@ -1,96 +0,0 @@
# 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,11 +3,13 @@ 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"
@ -37,6 +39,7 @@ func main() {
}
func visitRandomUser() error {
rand.Seed(time.Now().UnixNano())
usernames, err := getUsernames()
if err != nil {
return err
@ -75,7 +78,7 @@ func visitPrompt() error {
}
func visitUser(username string) error {
files, err := os.ReadDir(filepath.Join("/home", username))
files, err := ioutil.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)
}
@ -87,7 +90,7 @@ func visitUser(username string) error {
path := filepath.Join("/home", username, file.Name())
data, err := os.ReadFile(path)
data, err := ioutil.ReadFile(path)
if err != nil {
break
}

View File

@ -7,7 +7,6 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"time"
"git.tilde.town/tildetown/town/invites"
@ -143,7 +142,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 private key file" %[1]s@tilde.town
ssh -i "replace with path to public key file" %[1]s@tilde.town
for help with ssh, see: https://tilde.town/wiki/getting-started/ssh.html
@ -206,16 +205,12 @@ func createUser(data newUserData) (err error) {
}
func keyfileText(data newUserData) string {
pkey := data.PubKey
if !strings.HasSuffix(pkey, "\n") {
pkey += "\n"
}
header := `########## GREETINGS! ##########
# This file was automatically generated by tilde.town when
# Hi! 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, pkey)
return fmt.Sprintf("%s\n%s", header, data.PubKey)
}
func main() {

View File

@ -1,47 +0,0 @@
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,15 +4,13 @@ import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/mail"
"net/smtp"
"os/exec"
)
const (
SMTPHost = "smtp.migadu.com"
from = "root@tilde.town"
SMTPHost = "smtp.zoho.com"
SMTPPort = 465
)
@ -40,79 +38,61 @@ func NewExternalMailer(pw string) *ExternalMailer {
}
}
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}
func (m *ExternalMailer) Send(address, subject, body string) error {
headers := map[string]string{
"From": from,
"To": address,
"Subject": subject,
}
// 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
// Connect to the SMTP Server
servername := fmt.Sprintf("%s:%d", SMTPHost, SMTPPort)
auth := smtp.PlainAuth("", from, m.Password, SMTPHost)
host, _, _ := net.SplitHostPort(servername)
server := fmt.Sprintf("%s:%d", SMTPHost, SMTPPort)
auth := smtp.PlainAuth("", "root@tilde.town", m.Password, host)
// TLS config
tlsconfig := &tls.Config{
tlsconf := &tls.Config{
InsecureSkipVerify: true,
ServerName: host,
ServerName: server,
}
// 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)
conn, err := tls.Dial("tcp", server, tlsconf)
if err != nil {
return fmt.Errorf("failed dial: %w", err)
return err
}
c, err := smtp.NewClient(conn, host)
c, err := smtp.NewClient(conn, SMTPHost)
if err != nil {
return fmt.Errorf("failed to make smtp client: %w", err)
return err
}
// Auth
if err = c.Auth(auth); err != nil {
return fmt.Errorf("failed to make auth: %w", err)
return fmt.Errorf("auth failed for smtp: %w", err)
}
// To && From
if err = c.Mail(from.Address); err != nil {
return fmt.Errorf("failed to create mail: %w", err)
if err = c.Mail(from); err != nil {
return err
}
if err = c.Rcpt(to.Address); err != nil {
return fmt.Errorf("failed to add rcpt: %w", err)
if err = c.Rcpt(address); err != nil {
return err
}
// Data
w, err := c.Data()
if err != nil {
return fmt.Errorf("failed to send data: %w", err)
return err
}
_, err = w.Write([]byte(message))
if err != nil {
return fmt.Errorf("failed to write: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("failed to close: %w", err)
return err
}
w.Close()
c.Quit()
return nil

1
external/README.md vendored
View File

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

View File

@ -1,61 +0,0 @@
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)
}

View File

@ -1,404 +0,0 @@
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

@ -1,59 +0,0 @@
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

@ -1,40 +0,0 @@
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

@ -1,64 +0,0 @@
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,12 +6,9 @@ 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/huh v0.5.3
github.com/charmbracelet/lipgloss v0.13.0
github.com/charmbracelet/lipgloss v0.6.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
@ -19,42 +16,27 @@ 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.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/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/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.15.3-0.20240618155329-98d742f6907a // indirect
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rivo/uniseg v0.4.2 // 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/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/text v0.3.7 // indirect
)

74
go.sum
View File

@ -1,36 +1,17 @@
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/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/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
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=
@ -39,16 +20,10 @@ 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=
@ -59,41 +34,29 @@ 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.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-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-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/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.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
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.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/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/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=
@ -102,8 +65,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.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8=
github.com/rivo/uniseg v0.4.2/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=
@ -120,20 +83,14 @@ 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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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=
@ -141,9 +98,8 @@ 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,16 +1,20 @@
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
)
type Invite struct {
@ -29,7 +33,7 @@ func (i *Invite) Insert(db *sql.DB) error {
return err
}
i.Code = codes.NewCode(i.Email)
i.Code = generateCode(i.Email)
_, err = stmt.Exec(i.Code, i.Email)
if err != nil {
@ -49,6 +53,44 @@ 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,6 +12,7 @@ import (
"os/user"
"path/filepath"
"syscall"
"time"
email "git.tilde.town/tildetown/town/email"
)
@ -177,6 +178,7 @@ 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

@ -1,7 +0,0 @@
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,7 +2,7 @@ package towndb
import (
"database/sql"
"errors"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
@ -143,27 +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 = ?
`)
// TODO: really we should have like GetUser or something. but i don't want
// to have to populate the struct to do this operation for now ~equa
func RenameUser(db *sql.DB, old_name string, new_name string) (err error) {
stmt, err := db.Prepare(`UPDATE users SET username = ? WHERE username = ?`)
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 err
}
return u, nil
result, err := stmt.Exec(new_name, old_name)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return fmt.Errorf("couldn't find user")
}
return nil
}
func ConnectDB() (*sql.DB, error) {