From 09b991fc16e9c97e9aca75ffcc07a75ef16d02c8 Mon Sep 17 00:00:00 2001 From: Radon Rosborough Date: Mon, 23 Feb 2026 10:39:51 -0800 Subject: [PATCH] bridgev2/commands: Add captcha step type --- bridgev2/commands/login.go | 76 ++++++++++++++++++++++++++++++++++++++ bridgev2/login.go | 15 ++++++++ 2 files changed, 91 insertions(+) diff --git a/bridgev2/commands/login.go b/bridgev2/commands/login.go index 80a7c733..1bcb417a 100644 --- a/bridgev2/commands/login.go +++ b/bridgev2/commands/login.go @@ -18,6 +18,7 @@ import ( "github.com/skip2/go-qrcode" "go.mau.fi/util/curl" + "go.mau.fi/util/exmime" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" @@ -273,6 +274,44 @@ func sendQR(ce *Event, qr string, prevEventID *id.EventID) error { return nil } +func sendCaptcha(ce *Event, params *bridgev2.LoginCaptchaParams) error { + if params.ImageMimeType != "" { + filename := "captcha" + exmime.ExtensionFromMimetype(params.ImageMimeType) + mxc, file, err := ce.Bot.UploadMedia(ce.Ctx, ce.RoomID, params.ImageData, filename, params.ImageMimeType) + if err != nil { + return fmt.Errorf("failed to upload image: %w", err) + } + content := &event.MessageEventContent{ + MsgType: event.MsgImage, + FileName: filename, + URL: mxc, + File: file, + } + _, err = ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, nil) + if err != nil { + return nil + } + } + if params.AudioMimeType != "" { + filename := "captcha" + exmime.ExtensionFromMimetype(params.AudioMimeType) + mxc, file, err := ce.Bot.UploadMedia(ce.Ctx, ce.RoomID, params.AudioData, filename, params.AudioMimeType) + if err != nil { + return fmt.Errorf("failed to upload audio: %w", err) + } + content := &event.MessageEventContent{ + MsgType: event.MsgAudio, + FileName: filename, + URL: mxc, + File: file, + } + _, err = ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, nil) + if err != nil { + return nil + } + } + return nil +} + type contextKey int const ( @@ -463,6 +502,31 @@ func maybeURLDecodeCookie(val string, field *bridgev2.LoginCookieField) string { return decoded } +type captchaLoginCommandState struct { + Login bridgev2.LoginProcessCaptcha + Data *bridgev2.LoginCaptchaParams + Override *bridgev2.UserLogin +} + +func (clcs *captchaLoginCommandState) prompt(ce *Event) { + ce.Reply("Please enter the captcha code") + StoreCommandState(ce.User, &CommandState{ + Next: MinimalCommandHandlerFunc(clcs.submit), + Action: "Login", + Meta: clcs, + Cancel: clcs.Login.Cancel, + }) +} + +func (clcs *captchaLoginCommandState) submit(ce *Event) { + StoreCommandState(ce.User, nil) + if nextStep, err := clcs.Login.SubmitCaptcha(ce.Ctx, ce.RawArgs); err != nil { + ce.Reply("Failed to submit captcha: %v", err) + } else { + doLoginStep(ce, clcs.Login, nextStep, clcs.Override) + } +} + func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginStep, override *bridgev2.UserLogin) { if step.Instructions != "" { ce.Reply(step.Instructions) @@ -484,6 +548,18 @@ func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginSte Data: make(map[string]string), Override: override, }).promptNext(ce) + case bridgev2.LoginStepTypeCaptcha: + err := sendCaptcha(ce, step.CaptchaParams) + if err != nil { + ce.Reply("Failed to send captcha: %v", err) + login.Cancel() + return + } + (&captchaLoginCommandState{ + Login: login.(bridgev2.LoginProcessCaptcha), + Data: step.CaptchaParams, + Override: override, + }).prompt(ce) case bridgev2.LoginStepTypeComplete: if override != nil && override.ID != step.CompleteParams.UserLoginID { ce.Log.Info(). diff --git a/bridgev2/login.go b/bridgev2/login.go index 4ddbf13e..9e2bb4b3 100644 --- a/bridgev2/login.go +++ b/bridgev2/login.go @@ -58,6 +58,11 @@ type LoginProcessCookies interface { SubmitCookies(ctx context.Context, cookies map[string]string) (*LoginStep, error) } +type LoginProcessCaptcha interface { + LoginProcess + SubmitCaptcha(ctx context.Context, code string) (*LoginStep, error) +} + type LoginFlow struct { Name string `json:"name"` Description string `json:"description"` @@ -70,6 +75,7 @@ const ( LoginStepTypeUserInput LoginStepType = "user_input" LoginStepTypeCookies LoginStepType = "cookies" LoginStepTypeDisplayAndWait LoginStepType = "display_and_wait" + LoginStepTypeCaptcha LoginStepType = "captcha" LoginStepTypeComplete LoginStepType = "complete" ) @@ -99,6 +105,7 @@ type LoginStep struct { DisplayAndWaitParams *LoginDisplayAndWaitParams `json:"display_and_wait,omitempty"` CookiesParams *LoginCookiesParams `json:"cookies,omitempty"` UserInputParams *LoginUserInputParams `json:"user_input,omitempty"` + CaptchaParams *LoginCaptchaParams `json:"captcha,omitempty"` CompleteParams *LoginCompleteParams `json:"complete,omitempty"` } @@ -273,6 +280,14 @@ type LoginUserInputParams struct { Fields []LoginInputDataField `json:"fields"` } +type LoginCaptchaParams struct { + ImageData []byte `json:"image,omitempty"` + ImageMimeType string `json:"image_content_type"` + + AudioData []byte `json:"audio,omitempty"` + AudioMimeType string `json:"audio_content_type,omitempty"` +} + type LoginCompleteParams struct { UserLoginID networkid.UserLoginID `json:"user_login_id"` UserLogin *UserLogin `json:"-"`