Compare commits
3 Commits
470ebb5507
...
1bdd9249fc
Author | SHA1 | Date |
---|---|---|
vilmibm | 1bdd9249fc | |
vilmibm | 9464d393e3 | |
vilmibm | 8138733bd1 |
|
@ -0,0 +1,50 @@
|
|||
This is an implementation of an idea we discussed a while ago; a launcher for town-specific
|
||||
commands.
|
||||
|
||||
The idea is to put town commands in one of three places:
|
||||
|
||||
- /town/launcher/core
|
||||
- /town/launcher/contrib
|
||||
- /town/launcher/admin
|
||||
|
||||
and pair each command with a corresponding .yml file.
|
||||
|
||||
For example, the `aup` command is a simple wrapper around elinks that opens our code of conduct. I
|
||||
put the executable `aup` in /town/launcher/core and matched it with /town/launcher/aup.yml. The
|
||||
purpose of the yaml file is to provide documentation for your executable, so `aup.yml` looks like:
|
||||
|
||||
```yaml
|
||||
shortDesc: View the town's Acceptable Use Policy
|
||||
longDesc: |
|
||||
This command will open our code of conduct, a type of document that evokes the Acceptable Use
|
||||
Policies that governed servers like this in the past. It will open the elinks browser to a
|
||||
page on the wiki.
|
||||
examples: |
|
||||
$ town aup # open the aup
|
||||
$ town aup --rainbow # open the aup with rainbow colors
|
||||
maintainer: vilmibm
|
||||
```
|
||||
|
||||
and using the launcher is like:
|
||||
|
||||
$ town aup
|
||||
$ town aup --rainbow
|
||||
$ town writo
|
||||
$ town admin ban vilmibm
|
||||
|
||||
You can see all the commands with `town help` as well as their descriptions; `town help
|
||||
aup` would show you the docs from `aup.yml`.
|
||||
|
||||
I'd love feedback on this approach while I wrap up this implementation. I can put it up on
|
||||
git.tilde.town if anyone desires to collaborate (and let me know if you want a git.tilde.town
|
||||
account).
|
||||
|
||||
Remaining TODOs:
|
||||
|
||||
- [ ] make tab completion available for common shells
|
||||
- [ ] document / script submitting a tool for inclusion in contrib
|
||||
- [x] make little wrappers for things like `mail` and `chat`
|
||||
- [x] fix arg passing
|
||||
- [x] test with a command that makes use of stdin/stdout
|
||||
- [x] add all existing commands to the buckets
|
||||
- [x] add to users' paths
|
|
@ -0,0 +1,174 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const binroot = "/town/commands"
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "town",
|
||||
Short: "Run commands unique to tilde.town",
|
||||
}
|
||||
|
||||
var adminCmd = &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Run administrative commands",
|
||||
}
|
||||
|
||||
func isAdmin() (bool, error) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get information about current user: %w", err)
|
||||
}
|
||||
|
||||
adminGroup, err := user.LookupGroup("admin")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get admin group info: %w", err)
|
||||
}
|
||||
|
||||
groupIds, err := u.GroupIds()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get groups info: %w", err)
|
||||
}
|
||||
|
||||
for _, groupId := range groupIds {
|
||||
if groupId == adminGroup.Gid {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func parseCommands(targetCmd *cobra.Command, path string) error {
|
||||
binPath := filepath.Join(binroot, path)
|
||||
files, err := ioutil.ReadDir(binPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list directory %s: %s", binPath, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file.Name(), "yml") {
|
||||
parseCommand(targetCmd, filepath.Join(binPath, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type commandDoc struct {
|
||||
ShortDesc string `yaml:"shortDesc"`
|
||||
LongDesc string `yaml:"longDesc"`
|
||||
Examples string
|
||||
Maintainer string
|
||||
}
|
||||
|
||||
func parseCommand(targetCmd *cobra.Command, yamlPath string) {
|
||||
executablePath := strings.TrimSuffix(yamlPath, ".yml")
|
||||
|
||||
// TODO handle when files lack executable bit
|
||||
_, err := os.Stat(executablePath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not find matching executable for %s; skipping...\n", yamlPath)
|
||||
return
|
||||
}
|
||||
|
||||
yamlBytes, err := ioutil.ReadFile(yamlPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath)
|
||||
return
|
||||
}
|
||||
|
||||
doc := commandDoc{}
|
||||
err = yaml.Unmarshal(yamlBytes, &doc)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath)
|
||||
return
|
||||
}
|
||||
|
||||
if doc.Maintainer == "" {
|
||||
fmt.Fprintf(os.Stderr, "%s is missing maintainer field; skipping...\n", yamlPath)
|
||||
return
|
||||
}
|
||||
|
||||
parsedCmd := &cobra.Command{
|
||||
Use: filepath.Base(executablePath),
|
||||
RunE: execWrapper(executablePath),
|
||||
DisableFlagParsing: true,
|
||||
}
|
||||
if doc.ShortDesc != "" {
|
||||
parsedCmd.Short = doc.ShortDesc
|
||||
}
|
||||
|
||||
if doc.LongDesc != "" {
|
||||
parsedCmd.Long = doc.LongDesc
|
||||
}
|
||||
|
||||
parsedCmd.Long += fmt.Sprintf("\nMaintained by %s; reach out to them via mail or chat with questions", doc.Maintainer)
|
||||
|
||||
if doc.Examples != "" {
|
||||
parsedCmd.Example = doc.Examples
|
||||
}
|
||||
|
||||
targetCmd.AddCommand(parsedCmd)
|
||||
}
|
||||
|
||||
func execWrapper(executablePath string) func(*cobra.Command, []string) error {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
execCmd := exec.Command(executablePath, args...)
|
||||
execCmd.Stderr = os.Stderr
|
||||
execCmd.Stdout = os.Stdout
|
||||
execCmd.Stdin = os.Stdin
|
||||
return execCmd.Run()
|
||||
}
|
||||
}
|
||||
|
||||
func cli() int {
|
||||
err := parseCommands(rootCmd, "core")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to parse core commands: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
err = parseCommands(rootCmd, "contrib")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to parse contrib commands: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
admin, err := isAdmin()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to check admin status: %s", err)
|
||||
return 2
|
||||
}
|
||||
|
||||
if admin {
|
||||
rootCmd.AddCommand(adminCmd)
|
||||
err = parseCommands(adminCmd, "admin")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to parse admin commands: %s", err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// I feel like the example/documentation yaml can be frontmatter for non-binary files. to start
|
||||
// i'll just do the accompanying yaml file.
|
||||
rootCmd.Execute()
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func main() {
|
||||
os.Exit(cli())
|
||||
}
|
|
@ -6,11 +6,10 @@ import (
|
|||
"os"
|
||||
"os/user"
|
||||
|
||||
"git.tilde.town/tildetown/town/request"
|
||||
townUser "git.tilde.town/tildetown/town/user"
|
||||
)
|
||||
|
||||
const requestPath = "/town/requests"
|
||||
|
||||
func _main(args []string) error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
|
@ -27,12 +26,12 @@ func _main(args []string) error {
|
|||
|
||||
errs := []error{}
|
||||
|
||||
err = processGitea(requestPath)
|
||||
err = request.ProcessGitea(request.RequestPath)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = processGemini(requestPath)
|
||||
err = request.ProcessGemini(request.RequestPath)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
5
go.mod
5
go.mod
|
@ -1,3 +1,8 @@
|
|||
module git.tilde.town/tildetown/town
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/spf13/cobra v1.5.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
|
13
go.sum
13
go.sum
|
@ -0,0 +1,13 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
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=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
7
main.go
7
main.go
|
@ -1,7 +0,0 @@
|
|||
package main
|
||||
|
||||
import "git.tilde.town/tildetown/town/email"
|
||||
|
||||
func main() {
|
||||
email.SendLocalEmail("vilmibm", "testing hi", "this is a body")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package request
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
const geminiHomeDocBase = "/home/gemini/users"
|
||||
|
||||
func processGemini(requestRootPath string) error {
|
||||
func ProcessGemini(requestRootPath string) error {
|
||||
rp := filepath.Join(requestRootPath, "gemini")
|
||||
|
||||
files, err := ioutil.ReadDir(rp)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package request
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -19,13 +19,13 @@ import (
|
|||
|
||||
const pwLetters = "!@#$%^&*() []{}:;,.<>/?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
|
||||
|
||||
func processGitea(rp string) error {
|
||||
func ProcessGitea(rp string) error {
|
||||
apiToken := os.Getenv("GITEA_TOKEN")
|
||||
if apiToken == "" {
|
||||
return errors.New("need GITEA_TOKEN")
|
||||
}
|
||||
|
||||
gtPath := filepath.Join(requestPath, "gitea")
|
||||
gtPath := filepath.Join(RequestPath, "gitea")
|
||||
|
||||
files, err := ioutil.ReadDir(gtPath)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package request
|
||||
|
||||
const RequestPath = "/town/requests"
|
Loading…
Reference in New Issue