From 2279916d9cc34ffcafd7a93a843d85de10c058d5 Mon Sep 17 00:00:00 2001 From: Gary Kramlich Date: Thu, 30 Dec 2021 15:08:41 -0600 Subject: [PATCH] Implement the command parser Right now this just supports help and version, but will be getting new commands shortly. --- bridge/commandhandler.go | 112 +++++++++++++++++++++++++++++++++++++++ bridge/commands.go | 81 ++++++++++++++++++++++++++++ bridge/matrix.go | 26 ++++++++- bridge/portal.go | 14 +++++ bridge/puppet.go | 5 ++ config/bridge.go | 6 +++ go.mod | 1 + go.sum | 2 + 8 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 bridge/commandhandler.go create mode 100644 bridge/commands.go diff --git a/bridge/commandhandler.go b/bridge/commandhandler.go new file mode 100644 index 0000000..c5f91e8 --- /dev/null +++ b/bridge/commandhandler.go @@ -0,0 +1,112 @@ +package bridge + +import ( + "fmt" + "strings" + + "github.com/alecthomas/kong" + "github.com/google/shlex" + + "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/id" +) + +type commandHandler struct { + bridge *Bridge + log maulogger.Logger +} + +func newCommandHandler(bridge *Bridge) *commandHandler { + return &commandHandler{ + bridge: bridge, + log: bridge.log.Sub("Commands"), + } +} + +func commandsHelpPrinter(options kong.HelpOptions, ctx *kong.Context) error { + selected := ctx.Selected() + + if selected == nil { + for _, cmd := range ctx.Model.Leaves(true) { + fmt.Fprintf(ctx.Stdout, " * %s - %s\n", cmd.Path(), cmd.Help) + } + } else { + fmt.Fprintf(ctx.Stdout, "%s - %s\n", selected.Path(), selected.Help) + if selected.Detail != "" { + fmt.Fprintf(ctx.Stdout, "\n%s\n", selected.Detail) + } + if len(selected.Positional) > 0 { + fmt.Fprintf(ctx.Stdout, "\nArguments:\n") + for _, arg := range selected.Positional { + fmt.Fprintf(ctx.Stdout, "%s %s\n", arg.Summary(), arg.Help) + } + } + } + + return nil +} + +func (h *commandHandler) handle(roomID id.RoomID, user *User, message string, replyTo id.EventID) { + cmd := commands{ + globals: globals{ + bot: h.bridge.bot, + bridge: h.bridge, + portal: h.bridge.GetPortalByMXID(roomID), + handler: h, + roomID: roomID, + user: user, + replyTo: replyTo, + }, + } + + buf := &strings.Builder{} + + parse, err := kong.New( + &cmd, + kong.Exit(func(int) {}), + kong.NoDefaultHelp(), + kong.Writers(buf, buf), + kong.Help(commandsHelpPrinter), + ) + + if err != nil { + h.log.Warnf("Failed to create argument parser for %q: %v", roomID, err) + + cmd.globals.reply("unexpected error, please try again shortly") + + return + } + + args, err := shlex.Split(message) + if err != nil { + h.log.Warnf("Failed to split message %q: %v", message, err) + + cmd.globals.reply("failed to process the command") + + return + } + + ctx, err := parse.Parse(args) + if err != nil { + h.log.Warnf("Failed to parse command %q: %v", message, err) + + cmd.globals.reply("failed to process the command") + + return + } + + cmd.globals.context = ctx + + err = ctx.Run(&cmd.globals) + if err != nil { + h.log.Warnf("Command %q failed: %v", message, err) + + cmd.globals.reply("unexpected failure") + + return + } + + if buf.Len() > 0 { + cmd.globals.reply(buf.String()) + } +} diff --git a/bridge/commands.go b/bridge/commands.go new file mode 100644 index 0000000..30fa110 --- /dev/null +++ b/bridge/commands.go @@ -0,0 +1,81 @@ +package bridge + +import ( + "fmt" + + "github.com/alecthomas/kong" + + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" + + "gitlab.com/beeper/discord/consts" + "gitlab.com/beeper/discord/version" +) + +type globals struct { + context *kong.Context + + bridge *Bridge + bot *appservice.IntentAPI + portal *Portal + handler *commandHandler + roomID id.RoomID + user *User + replyTo id.EventID +} + +func (g *globals) reply(msg string) { + content := format.RenderMarkdown(msg, true, false) + content.MsgType = event.MsgNotice + intent := g.bot + + if g.portal != nil && g.portal.IsPrivateChat() { + intent = g.portal.MainIntent() + } + + _, err := intent.SendMessageEvent(g.roomID, event.EventMessage, content) + if err != nil { + g.handler.log.Warnfln("Failed to reply to command from %q: %v", g.user.MXID, err) + } +} + +type commands struct { + globals + + Help helpCmd `kong:"cmd,help='Displays this message.'"` + Version versionCmd `kong:"cmd,help='Displays the version of the bridge.'"` +} + +type helpCmd struct { + Command []string `kong:"arg,optional,help='The command to get help on.'"` +} + +func (c *helpCmd) Run(g *globals) error { + ctx, err := kong.Trace(g.context.Kong, c.Command) + if err != nil { + return err + } + + if ctx.Error != nil { + return err + } + + err = ctx.PrintUsage(true) + if err != nil { + return err + } + + fmt.Fprintln(g.context.Stdout) + + return nil +} + +type versionCmd struct{} + +func (c *versionCmd) Run(g *globals) error { + fmt.Fprintln(g.context.Stdout, consts.Name, version.String) + + return nil +} diff --git a/bridge/matrix.go b/bridge/matrix.go index 43f24cc..238b8ea 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -1,6 +1,8 @@ package bridge import ( + "strings" + "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" @@ -13,6 +15,7 @@ type matrixHandler struct { as *appservice.AppService bridge *Bridge log maulogger.Logger + cmd *commandHandler } func (b *Bridge) setupEvents() { @@ -22,6 +25,7 @@ func (b *Bridge) setupEvents() { as: b.as, bridge: b, log: b.log.Sub("Matrix"), + cmd: newCommandHandler(b), } b.eventProcessor.On(event.EventMessage, b.matrixHandler.handleMessage) @@ -61,11 +65,31 @@ func (mh *matrixHandler) ignoreEvent(evt *event.Event) bool { } func (mh *matrixHandler) handleMessage(evt *event.Event) { - mh.log.Debugfln("received message from %q: %q", evt.Sender, evt.Content.AsMessage()) if mh.ignoreEvent(evt) { return } + user := mh.bridge.GetUserByMXID(evt.Sender) + if user == nil { + return + } + + content := evt.Content.AsMessage() + content.RemoveReplyFallback() + + if content.MsgType == event.MsgText { + prefix := mh.bridge.config.Bridge.CommandPrefix + + hasPrefix := strings.HasPrefix(content.Body, prefix) + if hasPrefix { + content.Body = strings.TrimLeft(content.Body[len(prefix):], " ") + } + + if hasPrefix || evt.RoomID == user.ManagementRoom { + mh.cmd.handle(evt.RoomID, user, content.Body, content.GetReplyTo()) + return + } + } } func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { diff --git a/bridge/portal.go b/bridge/portal.go index f5f715e..570aaec 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -4,6 +4,8 @@ import ( "fmt" log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -93,3 +95,15 @@ func (p *Portal) messageLoop() { } } } + +func (p *Portal) IsPrivateChat() bool { + return false +} + +func (p *Portal) MainIntent() *appservice.IntentAPI { + if p.IsPrivateChat() { + return p.bridge.GetPuppetByID(p.Key.ID).DefaultIntent() + } + + return p.bridge.bot +} diff --git a/bridge/puppet.go b/bridge/puppet.go index 8d2423c..20f7fa1 100644 --- a/bridge/puppet.go +++ b/bridge/puppet.go @@ -5,6 +5,7 @@ import ( "regexp" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/id" "gitlab.com/beeper/discord/database" @@ -85,3 +86,7 @@ func (b *Bridge) FormatPuppetMXID(did string) id.UserID { b.config.Homeserver.Domain, ) } + +func (p *Puppet) DefaultIntent() *appservice.IntentAPI { + return p.bridge.as.Intent(p.MXID) +} diff --git a/config/bridge.go b/config/bridge.go index c287ff2..f057dad 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -8,6 +8,8 @@ import ( type bridge struct { UsernameTemplate string `yaml:"username_template"` + CommandPrefix string `yaml:"command_prefix"` + ManagementRoomText managementRoomText `yaml:"management_root_text"` PortalMessageBuffer int `yaml:"portal_message_buffer"` @@ -31,6 +33,10 @@ func (b *bridge) validate() error { return err } + if b.CommandPrefix == "" { + b.CommandPrefix = "!dis" + } + if err := b.ManagementRoomText.validate(); err != nil { return err } diff --git a/go.mod b/go.mod index 54812fc..4805960 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/btcsuite/btcutil v1.0.2 // indirect github.com/bwmarrin/discordgo v0.23.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 6c66625..e61b287 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=