diff --git a/Makefile b/Makefile index 92483f5..96121c5 100644 --- a/Makefile +++ b/Makefile @@ -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 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 diff --git a/cmd/sojuctl/main.go b/cmd/sojuctl/main.go new file mode 100644 index 0000000..19ddc77 --- /dev/null +++ b/cmd/sojuctl/main.go @@ -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] +` + +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() +}