2022-07-31 12:05:43 +00:00
package main
2022-07-31 13:43:29 +00:00
import (
"errors"
"fmt"
2022-10-08 04:44:15 +00:00
"math/rand"
2022-07-31 13:43:29 +00:00
"os"
2022-10-08 04:44:15 +00:00
"os/user"
"path"
2022-07-31 21:24:50 +00:00
"path/filepath"
2022-10-27 18:52:17 +00:00
"time"
2022-10-08 04:44:15 +00:00
"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"
2022-07-31 13:43:29 +00:00
)
2022-07-31 12:05:43 +00:00
2022-07-31 13:43:29 +00:00
const (
2022-10-08 04:44:15 +00:00
contribRequestPath = "/town/requests/contrib"
submitOp = "submit"
delOp = "del"
updateOp = "update"
2022-07-31 13:43:29 +00:00
)
2022-10-08 04:44:15 +00:00
type contribOpts struct {
2022-07-31 13:43:29 +00:00
CmdName string
2022-10-08 04:44:15 +00:00
Force bool // instantly install, do not make a request
2022-07-31 13:43:29 +00:00
Operation string // submit, delete, update
ExecPath string
}
2022-10-08 04:44:15 +00:00
type contrib struct {
CmdName string
ExecPath string
Maintainer string
MaintainerUID string
Category string
ShortDesc string
LongDesc string
2022-10-27 18:52:17 +00:00
Comments string
2022-10-08 04:44:15 +00:00
}
func runContrib ( opts * contribOpts ) error {
var action func ( * contribOpts ) error
2022-07-31 13:43:29 +00:00
switch opts . Operation {
case submitOp :
action = submit
case delOp :
action = delete
case updateOp :
action = update
}
return action ( opts )
}
2022-10-08 04:44:15 +00:00
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 {
2022-10-27 18:52:17 +00:00
rand . Seed ( time . Now ( ) . UTC ( ) . UnixNano ( ) )
2022-07-31 21:24:50 +00:00
var cmdName string
var category string
2022-10-08 04:44:15 +00:00
var shortDesc string
var longDesc string
2022-10-27 18:52:17 +00:00
var comments string
2022-10-08 04:44:15 +00:00
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.
2022-07-31 21:24:50 +00:00
2022-10-08 04:44:15 +00:00
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
}
2022-07-31 21:24:50 +00:00
_ , defaultName := filepath . Split ( opts . ExecPath )
if err := survey . AskOne ( & survey . Input {
2022-10-08 04:44:15 +00:00
Message : "what is the command's name?" ,
Help : "for example if it's called 'cats' it will be invoked when users run 'town cats'" ,
2022-07-31 21:24:50 +00:00
Default : defaultName } , & cmdName ) ; err != nil {
return err
}
categories := [ ] string {
"art" ,
"social" ,
"game" ,
"utility" ,
"misc" ,
}
var choice int
if err := survey . AskOne ( & survey . Select {
2022-10-08 04:44:15 +00:00
Message : "what category best describes this command?" ,
2022-07-31 21:24:50 +00:00
Options : categories ,
} , & choice ) ; err != nil {
return err
}
category = categories [ choice ]
if err := survey . AskOne ( & survey . Input {
2022-10-08 04:44:15 +00:00
Message : "in one brief sentence, what does this command do?" ,
} , & shortDesc ) ; err != nil {
2022-07-31 21:24:50 +00:00
return err
}
if err := survey . AskOne ( & survey . Editor {
2022-10-08 04:44:15 +00:00
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 {
2022-07-31 21:24:50 +00:00
return nil
}
2022-10-27 18:52:17 +00:00
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
}
2022-10-08 04:44:15 +00:00
// 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
2022-10-27 18:52:17 +00:00
Maintainer : u . Username ,
2022-10-08 04:44:15 +00:00
MaintainerUID : u . Uid ,
2022-10-27 18:52:17 +00:00
Comments : comments ,
2022-10-08 04:44:15 +00:00
}
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 )
2022-07-31 21:24:50 +00:00
2022-07-31 13:43:29 +00:00
return nil
}
2022-10-08 04:44:15 +00:00
func update ( opts * contribOpts ) error {
2022-07-31 13:43:29 +00:00
return nil
}
2022-10-08 04:44:15 +00:00
func delete ( opts * contribOpts ) error {
2022-07-31 13:43:29 +00:00
return nil
}
func rootCmd ( ) * cobra . Command {
var updateName string
var delName string
2022-10-08 04:44:15 +00:00
var force bool
2022-07-31 13:43:29 +00:00
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 ]
}
2022-10-08 04:44:15 +00:00
opts := & contribOpts {
2022-07-31 13:43:29 +00:00
Operation : operation ,
2022-10-08 04:44:15 +00:00
Force : force ,
2022-07-31 13:43:29 +00:00
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" )
2022-10-08 04:44:15 +00:00
rc . Flags ( ) . BoolVarP ( & force , "force" , "f" , false , "skip request, just install the command" )
2022-07-31 13:43:29 +00:00
return rc
}
2022-07-31 12:05:43 +00:00
func main ( ) {
2022-07-31 13:43:29 +00:00
if err := rootCmd ( ) . Execute ( ) ; err != nil {
fmt . Fprintln ( os . Stderr , err . Error ( ) )
os . Exit ( 1 )
}
2022-07-31 12:05:43 +00:00
}