diff --git a/bridgev2/bridge.go b/bridgev2/bridge.go index 36f7aa06..65d39c80 100644 --- a/bridgev2/bridge.go +++ b/bridgev2/bridge.go @@ -24,6 +24,10 @@ import ( var ErrNotLoggedIn = errors.New("not logged in") +type CommandProcessor interface { + Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *User, message string, replyTo id.EventID) +} + type Bridge struct { ID networkid.BridgeID DB *database.Database @@ -32,7 +36,7 @@ type Bridge struct { Matrix MatrixConnector Bot MatrixAPI Network NetworkConnector - Commands *CommandProcessor + Commands CommandProcessor Config *bridgeconfig.BridgeConfig DisappearLoop *DisappearLoop @@ -45,7 +49,15 @@ type Bridge struct { cacheLock sync.Mutex } -func NewBridge(bridgeID networkid.BridgeID, db *dbutil.Database, log zerolog.Logger, cfg *bridgeconfig.BridgeConfig, matrix MatrixConnector, network NetworkConnector) *Bridge { +func NewBridge( + bridgeID networkid.BridgeID, + db *dbutil.Database, + log zerolog.Logger, + cfg *bridgeconfig.BridgeConfig, + matrix MatrixConnector, + network NetworkConnector, + newCommandProcessor func(*Bridge) CommandProcessor, +) *Bridge { br := &Bridge{ ID: bridgeID, DB: database.New(bridgeID, db), @@ -64,7 +76,7 @@ func NewBridge(bridgeID networkid.BridgeID, db *dbutil.Database, log zerolog.Log if br.Config == nil { br.Config = &bridgeconfig.BridgeConfig{CommandPrefix: "!bridge"} } - br.Commands = NewProcessor(br) + br.Commands = newCommandProcessor(br) br.Matrix.Init(br) br.Bot = br.Matrix.BotIntent() br.Network.Init(br) diff --git a/bridgev2/cmdmeta.go b/bridgev2/cmdmeta.go deleted file mode 100644 index d866998f..00000000 --- a/bridgev2/cmdmeta.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2022 Tulir Asokan -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package bridgev2 - -var CommandHelp = &FullHandler{ - Func: func(ce *CommandEvent) { - ce.Reply(FormatHelp(ce)) - }, - Name: "help", - Help: HelpMeta{ - Section: HelpSectionGeneral, - Description: "Show this help message.", - }, -} - -var CommandCancel = &FullHandler{ - Func: func(ce *CommandEvent) { - state := ce.User.CommandState.Swap(nil) - if state != nil { - action := state.Action - if action == "" { - action = "Unknown action" - } - if state.Cancel != nil { - state.Cancel() - } - ce.Reply("%s cancelled.", action) - } else { - ce.Reply("No ongoing command.") - } - }, - Name: "cancel", - Help: HelpMeta{ - Section: HelpSectionGeneral, - Description: "Cancel an ongoing action.", - }, -} diff --git a/bridgev2/cmddebug.go b/bridgev2/commands/debug.go similarity index 86% rename from bridgev2/cmddebug.go rename to bridgev2/commands/debug.go index 400470ed..d00697ee 100644 --- a/bridgev2/cmddebug.go +++ b/bridgev2/commands/debug.go @@ -4,22 +4,23 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "strings" + "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" ) var CommandRegisterPush = &FullHandler{ - Func: func(ce *CommandEvent) { + Func: func(ce *Event) { if len(ce.Args) < 3 { ce.Reply("Usage: `$cmdprefix debug-register-push `\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins()) return } - pushType := PushTypeFromString(ce.Args[1]) - if pushType == PushTypeUnknown { + pushType := bridgev2.PushTypeFromString(ce.Args[1]) + if pushType == bridgev2.PushTypeUnknown { ce.Reply("Unknown push type `%s`. Allowed types: `web`, `apns`, `fcm`", ce.Args[1]) return } @@ -28,7 +29,7 @@ var CommandRegisterPush = &FullHandler{ ce.Reply("Login `%s` not found", ce.Args[0]) return } - pushable, ok := login.Client.(PushableNetworkAPI) + pushable, ok := login.Client.(bridgev2.PushableNetworkAPI) if !ok { ce.Reply("This network connector does not support push registration") return diff --git a/bridgev2/cmdevent.go b/bridgev2/commands/event.go similarity index 81% rename from bridgev2/cmdevent.go rename to bridgev2/commands/event.go index 0c80330d..d03c7e68 100644 --- a/bridgev2/cmdevent.go +++ b/bridgev2/commands/event.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "context" @@ -14,22 +14,24 @@ import ( "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" ) -// CommandEvent stores all data which might be used to handle commands -type CommandEvent struct { - Bot MatrixAPI - Bridge *Bridge - Portal *Portal - Processor *CommandProcessor +// Event stores all data which might be used to handle commands +type Event struct { + Bot bridgev2.MatrixAPI + Bridge *bridgev2.Bridge + Portal *bridgev2.Portal + Processor *Processor Handler MinimalCommandHandler RoomID id.RoomID EventID id.EventID - User *User + User *bridgev2.User Command string Args []string RawArgs string @@ -39,7 +41,7 @@ type CommandEvent struct { } // Reply sends a reply to command as notice, with optional string formatting and automatic $cmdprefix replacement. -func (ce *CommandEvent) Reply(msg string, args ...any) { +func (ce *Event) Reply(msg string, args ...any) { msg = strings.ReplaceAll(msg, "$cmdprefix ", ce.Bridge.Config.CommandPrefix+" ") if len(args) > 0 { msg = fmt.Sprintf(msg, args...) @@ -49,7 +51,7 @@ func (ce *CommandEvent) Reply(msg string, args ...any) { // ReplyAdvanced sends a reply to command as notice. It allows using HTML and disabling markdown, // but doesn't have built-in string formatting. -func (ce *CommandEvent) ReplyAdvanced(msg string, allowMarkdown, allowHTML bool) { +func (ce *Event) ReplyAdvanced(msg string, allowMarkdown, allowHTML bool) { content := format.RenderMarkdown(msg, allowMarkdown, allowHTML) content.MsgType = event.MsgNotice _, err := ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, time.Now()) @@ -59,7 +61,7 @@ func (ce *CommandEvent) ReplyAdvanced(msg string, allowMarkdown, allowHTML bool) } // React sends a reaction to the command. -func (ce *CommandEvent) React(key string) { +func (ce *Event) React(key string) { _, err := ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventReaction, &event.Content{ Parsed: &event.ReactionEventContent{ RelatesTo: event.RelatesTo{ @@ -75,7 +77,7 @@ func (ce *CommandEvent) React(key string) { } // Redact redacts the command. -func (ce *CommandEvent) Redact(req ...mautrix.ReqRedact) { +func (ce *Event) Redact(req ...mautrix.ReqRedact) { _, err := ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventRedaction, &event.Content{ Parsed: &event.RedactionEventContent{ Redacts: ce.EventID, @@ -87,7 +89,7 @@ func (ce *CommandEvent) Redact(req ...mautrix.ReqRedact) { } // MarkRead marks the command event as read. -func (ce *CommandEvent) MarkRead() { +func (ce *Event) MarkRead() { // TODO //err := ce.Bot.SendReceipt(ce.Ctx, ce.RoomID, ce.EventID, event.ReceiptTypeRead, nil) //if err != nil { diff --git a/bridgev2/cmdhandler.go b/bridgev2/commands/handler.go similarity index 85% rename from bridgev2/cmdhandler.go rename to bridgev2/commands/handler.go index 55db056f..b8ff7019 100644 --- a/bridgev2/cmdhandler.go +++ b/bridgev2/commands/handler.go @@ -4,19 +4,19 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "maunium.net/go/mautrix/event" ) type MinimalCommandHandler interface { - Run(*CommandEvent) + Run(*Event) } -type MinimalCommandHandlerFunc func(*CommandEvent) +type MinimalCommandHandlerFunc func(*Event) -func (mhf MinimalCommandHandlerFunc) Run(ce *CommandEvent) { +func (mhf MinimalCommandHandlerFunc) Run(ce *Event) { mhf(ce) } @@ -38,7 +38,7 @@ type AliasedCommandHandler interface { } type FullHandler struct { - Func func(*CommandEvent) + Func func(*Event) Name string Aliases []string @@ -64,12 +64,12 @@ func (fh *FullHandler) GetAliases() []string { return fh.Aliases } -func (fh *FullHandler) ShowInHelp(ce *CommandEvent) bool { +func (fh *FullHandler) ShowInHelp(ce *Event) bool { return true //return !fh.RequiresAdmin || ce.User.GetPermissionLevel() >= bridgeconfig.PermissionLevelAdmin } -func (fh *FullHandler) userHasRoomPermission(ce *CommandEvent) bool { +func (fh *FullHandler) userHasRoomPermission(ce *Event) bool { return true //levels, err := ce.MainIntent().PowerLevels(ce.Ctx, ce.RoomID) //if err != nil { @@ -80,7 +80,7 @@ func (fh *FullHandler) userHasRoomPermission(ce *CommandEvent) bool { //return levels.GetUserLevel(ce.User.GetMXID()) >= levels.GetEventLevel(fh.RequiresEventLevel) } -func (fh *FullHandler) Run(ce *CommandEvent) { +func (fh *FullHandler) Run(ce *Event) { //if fh.RequiresAdmin && ce.User.GetPermissionLevel() < bridgeconfig.PermissionLevelAdmin { // ce.Reply("That command is limited to bridge administrators.") //} else if fh.RequiresEventLevel.Type != "" && ce.User.GetPermissionLevel() < bridgeconfig.PermissionLevelAdmin && !fh.userHasRoomPermission(ce) { diff --git a/bridgev2/cmdhelp.go b/bridgev2/commands/help.go similarity index 92% rename from bridgev2/cmdhelp.go rename to bridgev2/commands/help.go index 80b8e972..5c91a4d1 100644 --- a/bridgev2/cmdhelp.go +++ b/bridgev2/commands/help.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "fmt" @@ -15,7 +15,7 @@ import ( type HelpfulHandler interface { CommandHandler GetHelp() HelpMeta - ShowInHelp(*CommandEvent) bool + ShowInHelp(*Event) bool } type HelpSection struct { @@ -78,7 +78,7 @@ func (h helpMetaList) Swap(i, j int) { var _ sort.Interface = (helpSectionList)(nil) var _ sort.Interface = (helpMetaList)(nil) -func FormatHelp(ce *CommandEvent) string { +func FormatHelp(ce *Event) string { sections := make(map[HelpSection]helpMetaList) for _, handler := range ce.Processor.handlers { helpfulHandler, ok := handler.(HelpfulHandler) @@ -128,3 +128,14 @@ func FormatHelp(ce *CommandEvent) string { } return output.String() } + +var CommandHelp = &FullHandler{ + Func: func(ce *Event) { + ce.Reply(FormatHelp(ce)) + }, + Name: "help", + Help: HelpMeta{ + Section: HelpSectionGeneral, + Description: "Show this help message.", + }, +} diff --git a/bridgev2/cmdlogin.go b/bridgev2/commands/login.go similarity index 84% rename from bridgev2/cmdlogin.go rename to bridgev2/commands/login.go index 160e6a96..b38bb21b 100644 --- a/bridgev2/cmdlogin.go +++ b/bridgev2/commands/login.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "context" @@ -18,6 +18,8 @@ import ( "github.com/skip2/go-qrcode" "golang.org/x/net/html" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -33,7 +35,7 @@ var CommandLogin = &FullHandler{ }, } -func formatFlowsReply(flows []LoginFlow) string { +func formatFlowsReply(flows []bridgev2.LoginFlow) string { var buf strings.Builder for _, flow := range flows { _, _ = fmt.Fprintf(&buf, "* `%s` - %s\n", flow.ID, flow.Description) @@ -41,7 +43,7 @@ func formatFlowsReply(flows []LoginFlow) string { return buf.String() } -func fnLogin(ce *CommandEvent) { +func fnLogin(ce *Event) { flows := ce.Bridge.Network.GetLoginFlows() var chosenFlowID string if len(ce.Args) > 0 { @@ -77,14 +79,14 @@ func fnLogin(ce *CommandEvent) { } type userInputLoginCommandState struct { - Login LoginProcessUserInput + Login bridgev2.LoginProcessUserInput Data map[string]string - RemainingFields []LoginInputDataField + RemainingFields []bridgev2.LoginInputDataField } -func (uilcs *userInputLoginCommandState) promptNext(ce *CommandEvent) { +func (uilcs *userInputLoginCommandState) promptNext(ce *Event) { // TODO reply prompting field - ce.User.CommandState.Store(&CommandState{ + StoreCommandState(ce.User, &CommandState{ Next: MinimalCommandHandlerFunc(uilcs.submitNext), Action: "Login", Meta: uilcs, @@ -92,7 +94,7 @@ func (uilcs *userInputLoginCommandState) promptNext(ce *CommandEvent) { }) } -func (uilcs *userInputLoginCommandState) submitNext(ce *CommandEvent) { +func (uilcs *userInputLoginCommandState) submitNext(ce *Event) { field := uilcs.RemainingFields[0] field.FillDefaultValidate() var err error @@ -105,7 +107,7 @@ func (uilcs *userInputLoginCommandState) submitNext(ce *CommandEvent) { uilcs.promptNext(ce) return } - ce.User.CommandState.Store(nil) + StoreCommandState(ce.User, nil) if nextStep, err := uilcs.Login.SubmitUserInput(ce.Ctx, uilcs.Data); err != nil { ce.Reply("Failed to submit input: %v", err) } else { @@ -115,7 +117,7 @@ func (uilcs *userInputLoginCommandState) submitNext(ce *CommandEvent) { const qrSizePx = 512 -func sendQR(ce *CommandEvent, qr string, prevEventID *id.EventID) error { +func sendQR(ce *Event, qr string, prevEventID *id.EventID) error { qrData, err := qrcode.Encode(qr, qrcode.Low, qrSizePx) if err != nil { return fmt.Errorf("failed to encode QR code: %w", err) @@ -153,25 +155,25 @@ const ( contextKeyPrevEventID contextKey = iota ) -func doLoginDisplayAndWait(ce *CommandEvent, login LoginProcessDisplayAndWait, step *LoginStep) { +func doLoginDisplayAndWait(ce *Event, login bridgev2.LoginProcessDisplayAndWait, step *bridgev2.LoginStep) { prevEvent, ok := ce.Ctx.Value(contextKeyPrevEventID).(*id.EventID) if !ok { prevEvent = new(id.EventID) ce.Ctx = context.WithValue(ce.Ctx, contextKeyPrevEventID, prevEvent) } switch step.DisplayAndWaitParams.Type { - case LoginDisplayTypeQR: + case bridgev2.LoginDisplayTypeQR: err := sendQR(ce, step.DisplayAndWaitParams.Data, prevEvent) if err != nil { ce.Reply("Failed to send QR code: %v", err) login.Cancel() return } - case LoginDisplayTypeEmoji: + case bridgev2.LoginDisplayTypeEmoji: ce.ReplyAdvanced(step.DisplayAndWaitParams.Data, false, false) - case LoginDisplayTypeCode: + case bridgev2.LoginDisplayTypeCode: ce.ReplyAdvanced(fmt.Sprintf("%s", html.EscapeString(step.DisplayAndWaitParams.Data)), false, true) - case LoginDisplayTypeNothing: + case bridgev2.LoginDisplayTypeNothing: // Do nothing default: ce.Reply("Unsupported display type %q", step.DisplayAndWaitParams.Type) @@ -196,12 +198,12 @@ func doLoginDisplayAndWait(ce *CommandEvent, login LoginProcessDisplayAndWait, s } type cookieLoginCommandState struct { - Login LoginProcessCookies - Data *LoginCookiesParams + Login bridgev2.LoginProcessCookies + Data *bridgev2.LoginCookiesParams } -func (clcs *cookieLoginCommandState) prompt(ce *CommandEvent) { - ce.User.CommandState.Store(&CommandState{ +func (clcs *cookieLoginCommandState) prompt(ce *Event) { + StoreCommandState(ce.User, &CommandState{ Next: MinimalCommandHandlerFunc(clcs.submit), Action: "Login", Meta: clcs, @@ -220,7 +222,7 @@ func missingKeys(required []string, data map[string]string) (missing []string) { return } -func (clcs *cookieLoginCommandState) submit(ce *CommandEvent) { +func (clcs *cookieLoginCommandState) submit(ce *Event) { ce.Redact() cookies := make(map[string]string) @@ -260,7 +262,7 @@ func (clcs *cookieLoginCommandState) submit(ce *CommandEvent) { ce.Reply("Missing required special keys: %+v", missingSpecial) return } - ce.User.CommandState.Store(nil) + StoreCommandState(ce.User, nil) nextStep, err := clcs.Login.SubmitCookies(ce.Ctx, cookies) if err != nil { ce.Reply("Login failed: %v", err) @@ -268,24 +270,24 @@ func (clcs *cookieLoginCommandState) submit(ce *CommandEvent) { doLoginStep(ce, clcs.Login, nextStep) } -func doLoginStep(ce *CommandEvent, login LoginProcess, step *LoginStep) { +func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginStep) { ce.Reply(step.Instructions) switch step.Type { - case LoginStepTypeDisplayAndWait: - doLoginDisplayAndWait(ce, login.(LoginProcessDisplayAndWait), step) - case LoginStepTypeCookies: + case bridgev2.LoginStepTypeDisplayAndWait: + doLoginDisplayAndWait(ce, login.(bridgev2.LoginProcessDisplayAndWait), step) + case bridgev2.LoginStepTypeCookies: (&cookieLoginCommandState{ - Login: login.(LoginProcessCookies), + Login: login.(bridgev2.LoginProcessCookies), Data: step.CookiesParams, }).prompt(ce) - case LoginStepTypeUserInput: + case bridgev2.LoginStepTypeUserInput: (&userInputLoginCommandState{ - Login: login.(LoginProcessUserInput), + Login: login.(bridgev2.LoginProcessUserInput), RemainingFields: step.UserInputParams.Fields, Data: make(map[string]string), }).promptNext(ce) - case LoginStepTypeComplete: + case bridgev2.LoginStepTypeComplete: // Nothing to do other than instructions default: panic(fmt.Errorf("unknown login step type %q", step.Type)) @@ -302,7 +304,7 @@ var CommandLogout = &FullHandler{ }, } -func fnLogout(ce *CommandEvent) { +func fnLogout(ce *Event) { if len(ce.Args) == 0 { ce.Reply("Usage: `$cmdprefix logout `\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins()) return @@ -328,7 +330,7 @@ var CommandSetPreferredLogin = &FullHandler{ RequiresPortal: true, } -func fnSetPreferredLogin(ce *CommandEvent) { +func fnSetPreferredLogin(ce *Event) { if len(ce.Args) == 0 { ce.Reply("Usage: `$cmdprefix set-preferred-login `\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins()) return diff --git a/bridgev2/cmdprocessor.go b/bridgev2/commands/processor.go similarity index 68% rename from bridgev2/cmdprocessor.go rename to bridgev2/commands/processor.go index 32428fc5..eb08cae2 100644 --- a/bridgev2/cmdprocessor.go +++ b/bridgev2/commands/processor.go @@ -4,32 +4,36 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "context" "fmt" "runtime/debug" "strings" + "sync/atomic" + "unsafe" "github.com/rs/zerolog" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridge/status" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) -type CommandProcessor struct { - bridge *Bridge +type Processor struct { + bridge *bridgev2.Bridge log *zerolog.Logger handlers map[string]CommandHandler aliases map[string]string } -// NewProcessor creates a CommandProcessor -func NewProcessor(bridge *Bridge) *CommandProcessor { - proc := &CommandProcessor{ +// NewProcessor creates a Processor +func NewProcessor(bridge *bridgev2.Bridge) bridgev2.CommandProcessor { + proc := &Processor{ bridge: bridge, log: &bridge.Log, @@ -45,13 +49,13 @@ func NewProcessor(bridge *Bridge) *CommandProcessor { return proc } -func (proc *CommandProcessor) AddHandlers(handlers ...CommandHandler) { +func (proc *Processor) AddHandlers(handlers ...CommandHandler) { for _, handler := range handlers { proc.AddHandler(handler) } } -func (proc *CommandProcessor) AddHandler(handler CommandHandler) { +func (proc *Processor) AddHandler(handler CommandHandler) { proc.handlers[handler.GetName()] = handler aliased, ok := handler.(AliasedCommandHandler) if ok { @@ -62,15 +66,15 @@ func (proc *CommandProcessor) AddHandler(handler CommandHandler) { } // Handle handles messages to the bridge -func (proc *CommandProcessor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *User, message string, replyTo id.EventID) { +func (proc *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *bridgev2.User, message string, replyTo id.EventID) { defer func() { - statusInfo := &MessageStatusEventInfo{ + statusInfo := &bridgev2.MessageStatusEventInfo{ RoomID: roomID, EventID: eventID, EventType: event.EventMessage, Sender: user.MXID, } - ms := MessageStatus{ + ms := bridgev2.MessageStatus{ Step: status.MsgStepCommand, Status: event.MessageStatusSuccess, } @@ -101,7 +105,7 @@ func (proc *CommandProcessor) Handle(ctx context.Context, roomID id.RoomID, even if err != nil { // :( } - ce := &CommandEvent{ + ce := &Event{ Bot: proc.bridge.Bot, Bridge: proc.bridge, Portal: portal, @@ -124,7 +128,7 @@ func (proc *CommandProcessor) Handle(ctx context.Context, roomID id.RoomID, even var handler MinimalCommandHandler handler, ok = proc.handlers[realCommand] if !ok { - state := ce.User.CommandState.Load() + state := LoadCommandState(ce.User) if state != nil && state.Next != nil { ce.Command = "" ce.RawArgs = message @@ -149,3 +153,38 @@ func (proc *CommandProcessor) Handle(ctx context.Context, roomID id.RoomID, even handler.Run(ce) } } + +func LoadCommandState(user *bridgev2.User) *CommandState { + return (*CommandState)(atomic.LoadPointer(&user.CommandState)) +} + +func StoreCommandState(user *bridgev2.User, cs *CommandState) { + atomic.StorePointer(&user.CommandState, unsafe.Pointer(cs)) +} + +func SwapCommandState(user *bridgev2.User, cs *CommandState) *CommandState { + return (*CommandState)(atomic.SwapPointer(&user.CommandState, unsafe.Pointer(cs))) +} + +var CommandCancel = &FullHandler{ + Func: func(ce *Event) { + state := SwapCommandState(ce.User, nil) + if state != nil { + action := state.Action + if action == "" { + action = "Unknown action" + } + if state.Cancel != nil { + state.Cancel() + } + ce.Reply("%s cancelled.", action) + } else { + ce.Reply("No ongoing command.") + } + }, + Name: "cancel", + Help: HelpMeta{ + Section: HelpSectionGeneral, + Description: "Cancel an ongoing action.", + }, +} diff --git a/bridgev2/cmdstartchat.go b/bridgev2/commands/startchat.go similarity index 91% rename from bridgev2/cmdstartchat.go rename to bridgev2/commands/startchat.go index a0530cdb..903fc17f 100644 --- a/bridgev2/cmdstartchat.go +++ b/bridgev2/commands/startchat.go @@ -4,7 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -package bridgev2 +package commands import ( "fmt" @@ -13,6 +13,8 @@ import ( "golang.org/x/net/html" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/id" ) @@ -39,7 +41,7 @@ var CommandStartChat = &FullHandler{ RequiresLogin: true, } -func getClientForStartingChat[T IdentifierResolvingNetworkAPI](ce *CommandEvent, thing string) (*UserLogin, T, []string) { +func getClientForStartingChat[T bridgev2.IdentifierResolvingNetworkAPI](ce *Event, thing string) (*bridgev2.UserLogin, T, []string) { remainingArgs := ce.Args[1:] login := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0])) if login == nil || login.UserMXID != ce.User.MXID { @@ -53,8 +55,8 @@ func getClientForStartingChat[T IdentifierResolvingNetworkAPI](ce *CommandEvent, return login, api, remainingArgs } -func fnResolveIdentifier(ce *CommandEvent) { - login, api, identifierParts := getClientForStartingChat[IdentifierResolvingNetworkAPI](ce, "resolving identifiers") +func fnResolveIdentifier(ce *Event) { + login, api, identifierParts := getClientForStartingChat[bridgev2.IdentifierResolvingNetworkAPI](ce, "resolving identifiers") if api == nil { return } @@ -125,7 +127,7 @@ func fnResolveIdentifier(ce *CommandEvent) { } var CommandDeletePortal = &FullHandler{ - Func: func(ce *CommandEvent) { + Func: func(ce *Event) { err := ce.Portal.Delete(ce.Ctx) if err != nil { ce.Reply("Failed to delete portal: %v", err) diff --git a/bridgev2/matrix/cmdadmin.go b/bridgev2/matrix/cmdadmin.go index 45a83b4f..0bd3eb82 100644 --- a/bridgev2/matrix/cmdadmin.go +++ b/bridgev2/matrix/cmdadmin.go @@ -9,12 +9,12 @@ package matrix import ( "strconv" - "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/id" ) -var CommandDiscardMegolmSession = &bridgev2.FullHandler{ - Func: func(ce *bridgev2.CommandEvent) { +var CommandDiscardMegolmSession = &commands.FullHandler{ + Func: func(ce *commands.Event) { matrix := ce.Bridge.Matrix.(*Connector) if matrix.Crypto == nil { ce.Reply("This bridge instance doesn't have end-to-bridge encryption enabled") @@ -25,14 +25,14 @@ var CommandDiscardMegolmSession = &bridgev2.FullHandler{ }, Name: "discard-megolm-session", Aliases: []string{"discard-session"}, - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionAdmin, + Help: commands.HelpMeta{ + Section: commands.HelpSectionAdmin, Description: "Discard the Megolm session in the room", }, RequiresAdmin: true, } -func fnSetPowerLevel(ce *bridgev2.CommandEvent) { +func fnSetPowerLevel(ce *commands.Event) { var level int var userID id.UserID var err error @@ -65,12 +65,12 @@ func fnSetPowerLevel(ce *bridgev2.CommandEvent) { } } -var CommandSetPowerLevel = &bridgev2.FullHandler{ +var CommandSetPowerLevel = &commands.FullHandler{ Func: fnSetPowerLevel, Name: "set-pl", Aliases: []string{"set-power-level"}, - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionAdmin, + Help: commands.HelpMeta{ + Section: commands.HelpSectionAdmin, Description: "Change the power level in a portal room.", Args: "[_user ID_] <_power level_>", }, diff --git a/bridgev2/matrix/cmddoublepuppet.go b/bridgev2/matrix/cmddoublepuppet.go index 13d24f54..29175138 100644 --- a/bridgev2/matrix/cmddoublepuppet.go +++ b/bridgev2/matrix/cmddoublepuppet.go @@ -7,21 +7,21 @@ package matrix import ( - "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/commands" ) -var CommandLoginMatrix = &bridgev2.FullHandler{ +var CommandLoginMatrix = &commands.FullHandler{ Func: fnLoginMatrix, Name: "login-matrix", - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionAuth, + Help: commands.HelpMeta{ + Section: commands.HelpSectionAuth, Description: "Enable double puppeting.", Args: "<_access token_>", }, RequiresLogin: true, } -func fnLoginMatrix(ce *bridgev2.CommandEvent) { +func fnLoginMatrix(ce *commands.Event) { if len(ce.Args) == 0 { ce.Reply("**Usage:** `login-matrix `") return @@ -34,16 +34,16 @@ func fnLoginMatrix(ce *bridgev2.CommandEvent) { } } -var CommandPingMatrix = &bridgev2.FullHandler{ +var CommandPingMatrix = &commands.FullHandler{ Func: fnPingMatrix, Name: "ping-matrix", - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionAuth, + Help: commands.HelpMeta{ + Section: commands.HelpSectionAuth, Description: "Ping the Matrix server with the double puppet.", }, } -func fnPingMatrix(ce *bridgev2.CommandEvent) { +func fnPingMatrix(ce *commands.Event) { intent := ce.User.DoublePuppet(ce.Ctx) if intent == nil { ce.Reply("You don't have double puppeting enabled.") @@ -62,17 +62,17 @@ func fnPingMatrix(ce *bridgev2.CommandEvent) { } } -var CommandLogoutMatrix = &bridgev2.FullHandler{ +var CommandLogoutMatrix = &commands.FullHandler{ Func: fnLogoutMatrix, Name: "logout-matrix", - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionAuth, + Help: commands.HelpMeta{ + Section: commands.HelpSectionAuth, Description: "Disable double puppeting.", }, RequiresLogin: true, } -func fnLogoutMatrix(ce *bridgev2.CommandEvent) { +func fnLogoutMatrix(ce *commands.Event) { if ce.User.AccessToken == "" { ce.Reply("You don't have double puppeting enabled.") return diff --git a/bridgev2/matrix/connector.go b/bridgev2/matrix/connector.go index 4b913c21..1910c48d 100644 --- a/bridgev2/matrix/connector.go +++ b/bridgev2/matrix/connector.go @@ -26,6 +26,7 @@ import ( "maunium.net/go/mautrix/bridge/status" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -117,7 +118,7 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) { br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent) br.Bot = br.AS.BotIntent() br.Crypto = NewCryptoHelper(br) - br.Bridge.Commands.AddHandlers( + br.Bridge.Commands.(*commands.Processor).AddHandlers( CommandDiscardMegolmSession, CommandSetPowerLevel, CommandLoginMatrix, CommandPingMatrix, CommandLogoutMatrix, ) diff --git a/bridgev2/matrix/mxmain/main.go b/bridgev2/matrix/mxmain/main.go index 9a5326a6..8434e9c7 100644 --- a/bridgev2/matrix/mxmain/main.go +++ b/bridgev2/matrix/mxmain/main.go @@ -31,6 +31,7 @@ import ( "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/bridgeconfig" + "maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/bridgev2/matrix" ) @@ -226,15 +227,15 @@ func (br *BridgeMain) Init() { br.initDB() br.Matrix = matrix.NewConnector(br.Config) br.Matrix.IgnoreUnsupportedServer = *ignoreUnsupportedServer - br.Bridge = bridgev2.NewBridge("", br.DB, *br.Log, &br.Config.Bridge, br.Matrix, br.Connector) + br.Bridge = bridgev2.NewBridge("", br.DB, *br.Log, &br.Config.Bridge, br.Matrix, br.Connector, commands.NewProcessor) br.Matrix.AS.DoublePuppetValue = br.Name - br.Bridge.Commands.AddHandler(&bridgev2.FullHandler{ - Func: func(ce *bridgev2.CommandEvent) { + br.Bridge.Commands.(*commands.Processor).AddHandler(&commands.FullHandler{ + Func: func(ce *commands.Event) { ce.Reply("[%s](%s) %s (%s)", br.Name, br.URL, br.LinkifiedVersion, br.BuildTime.Format(time.RFC1123)) }, Name: "version", - Help: bridgev2.HelpMeta{ - Section: bridgev2.HelpSectionGeneral, + Help: commands.HelpMeta{ + Section: commands.HelpSectionGeneral, Description: "Get the bridge version.", }, }) diff --git a/bridgev2/user.go b/bridgev2/user.go index 9fca8de3..50054a77 100644 --- a/bridgev2/user.go +++ b/bridgev2/user.go @@ -11,7 +11,7 @@ import ( "fmt" "strings" "sync" - "sync/atomic" + "unsafe" "github.com/rs/zerolog" "golang.org/x/exp/maps" @@ -27,7 +27,7 @@ type User struct { Bridge *Bridge Log zerolog.Logger - CommandState atomic.Pointer[CommandState] + CommandState unsafe.Pointer doublePuppetIntent MatrixAPI doublePuppetInitialized bool