forked from tildetown/town
finish first pass on submitting commands
parent
571c37b089
commit
4a2d31e5fd
|
@ -55,3 +55,13 @@ in addition to this new interface, i want to change how commands are tracked. th
|
||||||
|
|
||||||
- sqlite3. a little overkill.
|
- sqlite3. a little overkill.
|
||||||
- json file. a little underkill.
|
- 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
|
||||||
|
|
|
@ -3,27 +3,45 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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/AlecAivazis/survey/v2"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"os"
|
"gopkg.in/yaml.v3"
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
contribRequestPath = "/town/requests/contrib"
|
||||||
submitOp = "submit"
|
submitOp = "submit"
|
||||||
delOp = "del"
|
delOp = "del"
|
||||||
updateOp = "update"
|
updateOp = "update"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContribOptions struct {
|
type contribOpts struct {
|
||||||
CmdName string
|
CmdName string
|
||||||
|
Force bool // instantly install, do not make a request
|
||||||
Operation string // submit, delete, update
|
Operation string // submit, delete, update
|
||||||
Admin bool
|
|
||||||
ExecPath string
|
ExecPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func runContrib(opts *ContribOptions) error {
|
type contrib struct {
|
||||||
var action func(*ContribOptions) error
|
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 {
|
switch opts.Operation {
|
||||||
case submitOp:
|
case submitOp:
|
||||||
action = submit
|
action = submit
|
||||||
|
@ -36,15 +54,49 @@ func runContrib(opts *ContribOptions) error {
|
||||||
return action(opts)
|
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 cmdName string
|
||||||
var category string
|
var category string
|
||||||
var shortDescription string
|
var shortDesc string
|
||||||
var longDescription 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)
|
_, defaultName := filepath.Split(opts.ExecPath)
|
||||||
if err := survey.AskOne(&survey.Input{
|
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 {
|
Default: defaultName}, &cmdName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -58,44 +110,75 @@ func submit(opts *ContribOptions) error {
|
||||||
}
|
}
|
||||||
var choice int
|
var choice int
|
||||||
if err := survey.AskOne(&survey.Select{
|
if err := survey.AskOne(&survey.Select{
|
||||||
Message: "what category should this command be filed under?",
|
Message: "what category best describes this command?",
|
||||||
Options: categories,
|
Options: categories,
|
||||||
}, &choice); err != nil {
|
}, &choice); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
category = categories[choice]
|
category = categories[choice]
|
||||||
fmt.Printf("DBG %#v\n", category)
|
|
||||||
|
|
||||||
if err := survey.AskOne(&survey.Input{
|
if err := survey.AskOne(&survey.Input{
|
||||||
Message: "What's a one line description of this command?",
|
Message: "in one brief sentence, what does this command do?",
|
||||||
}, &shortDescription); err != nil {
|
}, &shortDesc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := survey.AskOne(&survey.Editor{
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO create pending row
|
// TODO be able to set a maintainer other than caller. this might only be if an admin.
|
||||||
// TODO email me
|
// TODO would be fun if it was a Select using a user list -- extract that from stats cmd
|
||||||
// TODO print confirmation
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(opts *ContribOptions) error {
|
func update(opts *contribOpts) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete(opts *ContribOptions) error {
|
func delete(opts *contribOpts) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootCmd() *cobra.Command {
|
func rootCmd() *cobra.Command {
|
||||||
var updateName string
|
var updateName string
|
||||||
var delName string
|
var delName string
|
||||||
var admin bool
|
var force bool
|
||||||
|
|
||||||
rc := &cobra.Command{
|
rc := &cobra.Command{
|
||||||
Use: "contrib [path to executable]",
|
Use: "contrib [path to executable]",
|
||||||
|
@ -135,9 +218,9 @@ func rootCmd() *cobra.Command {
|
||||||
execPath = args[0]
|
execPath = args[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &ContribOptions{
|
opts := &contribOpts{
|
||||||
Operation: operation,
|
Operation: operation,
|
||||||
Admin: admin,
|
Force: force,
|
||||||
ExecPath: execPath,
|
ExecPath: execPath,
|
||||||
CmdName: cmdName,
|
CmdName: cmdName,
|
||||||
}
|
}
|
||||||
|
@ -149,7 +232,7 @@ func rootCmd() *cobra.Command {
|
||||||
|
|
||||||
rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update")
|
rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update")
|
||||||
rc.Flags().StringVarP(&delName, "delete", "d", "", "Name of command to delete")
|
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
|
return rc
|
||||||
}
|
}
|
||||||
|
|
17
go.mod
17
go.mod
|
@ -10,11 +10,26 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
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/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-colorable v0.1.2 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.13 // 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/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/sys v0.0.0-20220422013727-9388b58f7150 // indirect
|
||||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
|
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
|
||||||
golang.org/x/text v0.3.6 // indirect
|
golang.org/x/text v0.3.6 // indirect
|
||||||
|
|
2
go.sum
2
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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
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.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 h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
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 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
|
||||||
|
|
Loading…
Reference in New Issue