Compare commits

...

3 Commits

Author SHA1 Message Date
vilmibm 1bdd9249fc copy paste launcher 2022-07-30 13:45:37 +00:00
vilmibm 9464d393e3 fix var 2022-07-30 13:42:33 +00:00
vilmibm 8138733bd1 move request code 2022-07-30 13:39:48 +00:00
9 changed files with 253 additions and 16 deletions

View File

@ -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

View File

@ -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())
}

View File

@ -6,11 +6,10 @@ import (
"os" "os"
"os/user" "os/user"
"git.tilde.town/tildetown/town/request"
townUser "git.tilde.town/tildetown/town/user" townUser "git.tilde.town/tildetown/town/user"
) )
const requestPath = "/town/requests"
func _main(args []string) error { func _main(args []string) error {
currentUser, err := user.Current() currentUser, err := user.Current()
if err != nil { if err != nil {
@ -27,12 +26,12 @@ func _main(args []string) error {
errs := []error{} errs := []error{}
err = processGitea(requestPath) err = request.ProcessGitea(request.RequestPath)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
err = processGemini(requestPath) err = request.ProcessGemini(request.RequestPath)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
} }

5
go.mod
View File

@ -1,3 +1,8 @@
module git.tilde.town/tildetown/town module git.tilde.town/tildetown/town
go 1.14 go 1.14
require (
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
)

13
go.sum
View File

@ -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=

View File

@ -1,7 +0,0 @@
package main
import "git.tilde.town/tildetown/town/email"
func main() {
email.SendLocalEmail("vilmibm", "testing hi", "this is a body")
}

View File

@ -1,4 +1,4 @@
package main package request
import ( import (
"fmt" "fmt"
@ -11,7 +11,7 @@ import (
const geminiHomeDocBase = "/home/gemini/users" const geminiHomeDocBase = "/home/gemini/users"
func processGemini(requestRootPath string) error { func ProcessGemini(requestRootPath string) error {
rp := filepath.Join(requestRootPath, "gemini") rp := filepath.Join(requestRootPath, "gemini")
files, err := ioutil.ReadDir(rp) files, err := ioutil.ReadDir(rp)

View File

@ -1,4 +1,4 @@
package main package request
import ( import (
"bytes" "bytes"
@ -19,13 +19,13 @@ import (
const pwLetters = "!@#$%^&*() []{}:;,.<>/?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890" const pwLetters = "!@#$%^&*() []{}:;,.<>/?abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"
func processGitea(rp string) error { func ProcessGitea(rp string) error {
apiToken := os.Getenv("GITEA_TOKEN") apiToken := os.Getenv("GITEA_TOKEN")
if apiToken == "" { if apiToken == "" {
return errors.New("need GITEA_TOKEN") return errors.New("need GITEA_TOKEN")
} }
gtPath := filepath.Join(requestPath, "gitea") gtPath := filepath.Join(RequestPath, "gitea")
files, err := ioutil.ReadDir(gtPath) files, err := ioutil.ReadDir(gtPath)
if err != nil { if err != nil {

View File

@ -0,0 +1,3 @@
package request
const RequestPath = "/town/requests"