diff --git a/cmd/contrib/README.md b/cmd/contrib/README.md index 1d2cd7a..60058f5 100644 --- a/cmd/contrib/README.md +++ b/cmd/contrib/README.md @@ -55,3 +55,13 @@ in addition to this new interface, i want to change how commands are tracked. th - sqlite3. a little overkill. - json file. a little underkill. + + what am I doing. should there even be a system like this? what about just letting anyone add anything? i think we're big enough that we shouldn't go moderation less and i can also help guarantee things are runnable and maintainable, this way. + + i have a gut feeling that involving SQL here is bad. I like the flat file approach for requesting that something be added to core; those are easy to review and tweak, but after that, what to do? right now the town launcher consumes a directory of flat files to create the command hierarchy. i could keep that code the same. the ultimate issue is having to have the corresponding executable. right now, that might be a bash wrapper that i write; it could be a compiled executable; it could be a script someone else writes; as of now, it doesn't seem like it can be a symlink which i think is good. + + several of the wrapper scripts are just calling the executable path and passing args; i think i should just do that from go. + + so we'll have: /town/requests/contrib to store requests to add. once added, they will go to /town/commands/contrib as a yaml file with no need for a corresponding executable. + + competing with this, i want a `town wild` command that can call any executable in a world writeable dir. because it's funny diff --git a/cmd/contrib/main.go b/cmd/contrib/main.go index b829e33..018d7d7 100644 --- a/cmd/contrib/main.go +++ b/cmd/contrib/main.go @@ -3,27 +3,45 @@ package main import ( "errors" "fmt" + "math/rand" + "os" + "os/user" + "path" + "path/filepath" + + "git.tilde.town/tildetown/town/email" + tuser "git.tilde.town/tildetown/town/user" "github.com/AlecAivazis/survey/v2" "github.com/spf13/cobra" - "os" - "path/filepath" + "gopkg.in/yaml.v3" ) const ( - submitOp = "submit" - delOp = "del" - updateOp = "update" + contribRequestPath = "/town/requests/contrib" + submitOp = "submit" + delOp = "del" + updateOp = "update" ) -type ContribOptions struct { +type contribOpts struct { CmdName string + Force bool // instantly install, do not make a request Operation string // submit, delete, update - Admin bool ExecPath string } -func runContrib(opts *ContribOptions) error { - var action func(*ContribOptions) error +type contrib struct { + CmdName string + ExecPath string + Maintainer string + MaintainerUID string + Category string + ShortDesc string + LongDesc string +} + +func runContrib(opts *contribOpts) error { + var action func(*contribOpts) error switch opts.Operation { case submitOp: action = submit @@ -36,15 +54,49 @@ func runContrib(opts *ContribOptions) error { return action(opts) } -func submit(opts *ContribOptions) error { +func validExec(execPath string) error { + if !path.IsAbs(execPath) { + return fmt.Errorf("'%s' needs to be an absolute path", execPath) + } + fi, err := os.Stat(execPath) + if err != nil { + return fmt.Errorf("could not stat '%s'; does it exist? error: %w", execPath, err) + } + if fi.Mode()&0001 == 0 { + return fmt.Errorf("'%s' is not executable", execPath) + } + return nil +} + +func submit(opts *contribOpts) error { var cmdName string var category string - var shortDescription string - var longDescription string + var shortDesc string + var longDesc string + u, err := user.Current() + if err != nil { + return fmt.Errorf("that's my purse. I don't know you! %w", err) + } + + // TODO should commands be markable as for admins only? i feel like that can be a parallel, more manual system. + + isAdmin, err := tuser.IsAdmin(u) + if err != nil { + return fmt.Errorf("that's my purse. I don't know you! %w", err) + } + + if opts.Force && !isAdmin { + return errors.New("must be admin to use --force") + } + + if err := validExec(opts.ExecPath); err != nil { + return err + } _, defaultName := filepath.Split(opts.ExecPath) if err := survey.AskOne(&survey.Input{ - Message: "New command's name:", + Message: "what is the command's name?", + Help: "for example if it's called 'cats' it will be invoked when users run 'town cats'", Default: defaultName}, &cmdName); err != nil { return err } @@ -58,44 +110,75 @@ func submit(opts *ContribOptions) error { } var choice int if err := survey.AskOne(&survey.Select{ - Message: "what category should this command be filed under?", + Message: "what category best describes this command?", Options: categories, }, &choice); err != nil { return err } category = categories[choice] - fmt.Printf("DBG %#v\n", category) if err := survey.AskOne(&survey.Input{ - Message: "What's a one line description of this command?", - }, &shortDescription); err != nil { + Message: "in one brief sentence, what does this command do?", + }, &shortDesc); err != nil { return err } if err := survey.AskOne(&survey.Editor{ - Message: "Write a long description for this command. Try to include examples. If the short description is enough, you can just leave it blank though. Really it's whatever you want. I donno"}, &longDescription); err != nil { + Message: "yr editor is gonna open. in there please write a long description for this command. not like, novel long, but maybe a sentence or two and some examples."}, &longDesc); err != nil { return nil } - // TODO create pending row - // TODO email me - // TODO print confirmation + // TODO be able to set a maintainer other than caller. this might only be if an admin. + // TODO would be fun if it was a Select using a user list -- extract that from stats cmd + + c := contrib{ + CmdName: cmdName, + Category: category, + ShortDesc: shortDesc, + LongDesc: longDesc, + ExecPath: opts.ExecPath, + // for later validation against file owner + Maintainer: u.Name, + MaintainerUID: u.Uid, + } + + bs, err := yaml.Marshal(c) + if err != nil { + return fmt.Errorf("failed to serialize contrib: %w", err) + } + + fname := fmt.Sprintf("%d", rand.Intn(10000)) + f, err := os.Create(path.Join(contribRequestPath, fname)) + if err != nil { + return fmt.Errorf("failed to open contrib file for writing: %w", err) + } + + if _, err = f.Write(bs); err != nil { + return fmt.Errorf("failed to write contrib file: %w", err) + } + + err = email.SendLocalEmail("vilmibm", fmt.Sprintf("contrib: %s", cmdName), string(bs)) + if err != nil { + return fmt.Errorf("could not email vilmibm. ping them on IRC or something. the error was: %w", err) + } + + fmt.Printf("submitted %s for review! thank you~\n", cmdName) return nil } -func update(opts *ContribOptions) error { +func update(opts *contribOpts) error { return nil } -func delete(opts *ContribOptions) error { +func delete(opts *contribOpts) error { return nil } func rootCmd() *cobra.Command { var updateName string var delName string - var admin bool + var force bool rc := &cobra.Command{ Use: "contrib [path to executable]", @@ -135,9 +218,9 @@ func rootCmd() *cobra.Command { execPath = args[0] } - opts := &ContribOptions{ + opts := &contribOpts{ Operation: operation, - Admin: admin, + Force: force, ExecPath: execPath, CmdName: cmdName, } @@ -149,7 +232,7 @@ func rootCmd() *cobra.Command { rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update") rc.Flags().StringVarP(&delName, "delete", "d", "", "Name of command to delete") - rc.Flags().BoolVarP(&admin, "admin", "a", false, "admin level operations") + rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command") return rc } diff --git a/db/db.go b/db/db.go deleted file mode 100644 index d34f0da..0000000 --- a/db/db.go +++ /dev/null @@ -1,5 +0,0 @@ -package db - -const Path = "/town/var/town.sqlite3" - -// TODO diff --git a/go.mod b/go.mod index 8d865e6..6d1c289 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,26 @@ require ( ) require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/dlclark/regexp2 v1.4.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.13 // indirect - github.com/mattn/go-sqlite3 v1.14.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/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.9.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/rivo/uniseg v0.2.0 // 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/term v0.0.0-20210503060354-a79de5458b56 // indirect golang.org/x/text v0.3.6 // indirect diff --git a/go.sum b/go.sum index b792e9d..b465f7b 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m 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-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 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=