forked from tildetown/town
258 lines
6.1 KiB
Go
258 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.tilde.town/tildetown/town/email"
|
|
tuser "git.tilde.town/tildetown/town/user"
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/spf13/cobra"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
contribRequestPath = "/town/requests/contrib"
|
|
submitOp = "submit"
|
|
delOp = "del"
|
|
updateOp = "update"
|
|
)
|
|
|
|
type contribOpts struct {
|
|
CmdName string
|
|
Force bool // instantly install, do not make a request
|
|
Operation string // submit, delete, update
|
|
ExecPath string
|
|
}
|
|
|
|
type contrib struct {
|
|
CmdName string
|
|
ExecPath string
|
|
Maintainer string
|
|
MaintainerUID string
|
|
Category string
|
|
ShortDesc string
|
|
LongDesc string
|
|
Comments string
|
|
}
|
|
|
|
func runContrib(opts *contribOpts) error {
|
|
var action func(*contribOpts) error
|
|
switch opts.Operation {
|
|
case submitOp:
|
|
action = submit
|
|
case delOp:
|
|
action = delete
|
|
case updateOp:
|
|
action = update
|
|
}
|
|
|
|
return action(opts)
|
|
}
|
|
|
|
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 {
|
|
rand.Seed(time.Now().UTC().UnixNano())
|
|
|
|
var cmdName string
|
|
var category string
|
|
var shortDesc string
|
|
var longDesc string
|
|
var comments 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: "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
|
|
}
|
|
|
|
categories := []string{
|
|
"art",
|
|
"social",
|
|
"game",
|
|
"utility",
|
|
"misc",
|
|
}
|
|
var choice int
|
|
if err := survey.AskOne(&survey.Select{
|
|
Message: "what category best describes this command?",
|
|
Options: categories,
|
|
}, &choice); err != nil {
|
|
return err
|
|
}
|
|
category = categories[choice]
|
|
|
|
if err := survey.AskOne(&survey.Input{
|
|
Message: "in one brief sentence, what does this command do?",
|
|
}, &shortDesc); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := survey.AskOne(&survey.Editor{
|
|
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
|
|
}
|
|
|
|
if err := survey.AskOne(&survey.Multiline{
|
|
Message: "any comments for the admins? this won't be public, it's just to give admins any additional context about this command.",
|
|
}, &comments); err != nil {
|
|
return nil
|
|
}
|
|
|
|
// 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.Username,
|
|
MaintainerUID: u.Uid,
|
|
Comments: comments,
|
|
}
|
|
|
|
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 *contribOpts) error {
|
|
return nil
|
|
}
|
|
|
|
func delete(opts *contribOpts) error {
|
|
return nil
|
|
}
|
|
|
|
func rootCmd() *cobra.Command {
|
|
var updateName string
|
|
var delName string
|
|
var force bool
|
|
|
|
rc := &cobra.Command{
|
|
Use: "contrib [path to executable]",
|
|
Short: "Submit new commands to the town launcher",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// submit - requires path arg
|
|
// update - requres path arg
|
|
// delete - no arg needed
|
|
if updateName != "" && delName != "" {
|
|
return errors.New("-u and -d are mutually exclusive")
|
|
}
|
|
|
|
var cmdName string
|
|
operation := "submit"
|
|
if updateName != "" {
|
|
operation = "update"
|
|
cmdName = updateName
|
|
} else if delName != "" {
|
|
operation = "delete"
|
|
cmdName = delName
|
|
}
|
|
|
|
if (operation == "update" || operation == "submit") && len(args) == 0 {
|
|
return errors.New("path to executable required when submitting or updating")
|
|
}
|
|
|
|
if operation == "delete" && len(args) > 0 {
|
|
return fmt.Errorf("no arguments expected when deleting; got %d", len(args))
|
|
}
|
|
|
|
if len(args) > 1 {
|
|
return fmt.Errorf("at most one argument is accepted; got %d", len(args))
|
|
}
|
|
|
|
var execPath string
|
|
if len(args) == 1 {
|
|
execPath = args[0]
|
|
}
|
|
|
|
opts := &contribOpts{
|
|
Operation: operation,
|
|
Force: force,
|
|
ExecPath: execPath,
|
|
CmdName: cmdName,
|
|
}
|
|
|
|
return runContrib(opts)
|
|
},
|
|
// TODO longer example
|
|
}
|
|
|
|
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(&force, "force", "f", false, "skip request, just install the command")
|
|
|
|
return rc
|
|
}
|
|
|
|
func main() {
|
|
if err := rootCmd().Execute(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|