Add cmd/sojuctl

This new command enables communicating with the unix administrative
endpoint (unix+admin) that can be enabled on soju.

The syntax is just that of BouncerServ.

Examples:
    sojuctl -config soju.config help
    sojuctl -config soju.config user status
This commit is contained in:
delthas 2023-01-20 15:56:04 +01:00 committed by Simon Ser
parent 6fe955e7ff
commit f57492af56
2 changed files with 116 additions and 4 deletions

View file

@ -11,24 +11,26 @@ config_path := $(DESTDIR)/$(SYSCONFDIR)/soju/config
goflags := $(GOFLAGS) \
-ldflags="-X 'git.sr.ht/~emersion/soju/config.DefaultPath=$(config_path)'"
all: soju sojudb doc/soju.1
all: soju sojudb sojuctl doc/soju.1
soju:
$(GO) build $(goflags) ./cmd/soju
sojudb:
$(GO) build $(goflags) ./cmd/sojudb
sojuctl:
$(GO) build $(goflags) ./cmd/sojuctl
doc/soju.1: doc/soju.1.scd
$(SCDOC) <doc/soju.1.scd >doc/soju.1
clean:
$(RM) -f soju sojudb doc/soju.1
$(RM) -f soju sojudb sojuctl doc/soju.1
install:
mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR)
mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR)/man1
mkdir -p $(DESTDIR)/$(SYSCONFDIR)/soju
mkdir -p $(DESTDIR)/var/lib/soju
cp -f soju sojudb $(DESTDIR)$(PREFIX)/$(BINDIR)
cp -f soju sojudb sojuctl $(DESTDIR)$(PREFIX)/$(BINDIR)
cp -f doc/soju.1 $(DESTDIR)$(PREFIX)/$(MANDIR)/man1
[ -f $(config_path) ] || cp -f config.in $(config_path)
.PHONY: soju sojudb clean install
.PHONY: soju sojudb sojuctl clean install

110
cmd/sojuctl/main.go Normal file
View file

@ -0,0 +1,110 @@
package main
import (
"context"
"flag"
"fmt"
"git.sr.ht/~emersion/soju"
"gopkg.in/irc.v4"
"log"
"net"
"net/url"
"strconv"
"strings"
"git.sr.ht/~emersion/soju/config"
)
const usage = `usage: sojuctl [-config path] <command>
`
func init() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage)
}
}
func run(ctx context.Context, cfg *config.Server, words []string) error {
var path string
for _, listen := range cfg.Listen {
u, err := url.Parse(listen)
if err != nil {
continue
}
if u.Scheme != "unix+admin" {
continue
}
if u.Path != "" {
path = u.Path
} else {
path = soju.DefaultUnixAdminPath
}
break
}
if path == "" {
return fmt.Errorf("no listen unix+admin directive found in config")
}
var d net.Dialer
uc, err := d.DialContext(ctx, "unix", path)
if err != nil {
return fmt.Errorf("dial %v: %v", path, err)
}
defer uc.Close()
c := irc.NewConn(uc)
if err := c.WriteMessage(&irc.Message{
Command: "BOUNCERSERV",
Params: []string{quoteWords(words)},
}); err != nil {
return fmt.Errorf("write: %v", err)
}
for {
m, err := c.ReadMessage()
if err != nil {
return fmt.Errorf("read: %v", err)
}
switch m.Command {
case "PRIVMSG":
fmt.Println(m.Trailing())
case "BOUNCERSERV":
if m.Param(0) == "OK" {
return nil
}
return fmt.Errorf(m.Trailing())
default:
return fmt.Errorf(m.Trailing())
}
}
}
func main() {
var configPath string
flag.StringVar(&configPath, "config", config.DefaultPath, "path to configuration file")
flag.Parse()
var cfg *config.Server
if configPath != "" {
var err error
cfg, err = config.Load(configPath)
if err != nil {
log.Fatalf("failed to load config file: %v", err)
}
} else {
cfg = config.Defaults()
}
ctx := context.Background()
if err := run(ctx, cfg, flag.Args()); err != nil {
log.Fatalln(err)
}
}
func quoteWords(words []string) string {
var s strings.Builder
for _, word := range words {
if s.Len() > 0 {
s.WriteRune(' ')
}
s.WriteString(strconv.Quote(word))
}
return s.String()
}