Compare commits
No commits in common. "1bdd9249fcce354c69739b126d4bcd78be83f433" and "470ebb550747c281cb9f9fe8932a96ddd6a5aec4" have entirely different histories.
1bdd9249fc
...
470ebb5507
|
@ -1,50 +0,0 @@
|
||||||
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
|
|
|
@ -1,174 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
5
go.mod
5
go.mod
|
@ -1,8 +1,3 @@
|
||||||
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
13
go.sum
|
@ -1,13 +0,0 @@
|
||||||
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=
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
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 request
|
package main
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package request
|
package main
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
@ -6,10 +6,11 @@ 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 {
|
||||||
|
@ -26,12 +27,12 @@ func _main(args []string) error {
|
||||||
|
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
|
|
||||||
err = request.ProcessGitea(request.RequestPath)
|
err = processGitea(requestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = request.ProcessGemini(request.RequestPath)
|
err = processGemini(requestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
package request
|
|
||||||
|
|
||||||
const RequestPath = "/town/requests"
|
|
Loading…
Reference in New Issue