diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index be7766ff..7d62eb47 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "log/slog" "math" + "math/rand" "os" "os/exec" "strconv" @@ -15,6 +17,7 @@ import ( "time" "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/mousetrajectory" oapi "github.com/onkernel/kernel-images/server/lib/oapi" ) @@ -51,34 +54,32 @@ func (s *ApiService) doMoveMouse(ctx context.Context, body oapi.MoveMouseRequest return &validationError{msg: fmt.Sprintf("coordinates exceed screen bounds (max: %dx%d)", screenWidth-1, screenHeight-1)} } - // Build xdotool arguments - args := []string{} + useSmooth := body.Smooth == nil || *body.Smooth // default true when omitted + if useSmooth { + return s.doMoveMouseSmooth(ctx, log, body) + } + return s.doMoveMouseInstant(ctx, log, body) +} - // Hold modifier keys (keydown) +func (s *ApiService) doMoveMouseInstant(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { + args := []string{} if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keydown", key) } } - - // Move the cursor to the desired coordinates args = append(args, "mousemove", strconv.Itoa(body.X), strconv.Itoa(body.Y)) - - // Release modifier keys (keyup) if body.HoldKeys != nil { for _, key := range *body.HoldKeys { args = append(args, "keyup", key) } } - log.Info("executing xdotool", "args", args) - output, err := defaultXdoTool.Run(ctx, args...) if err != nil { log.Error("xdotool command failed", "err", err, "output", string(output)) return &executionError{msg: "failed to move mouse"} } - return nil } @@ -100,6 +101,111 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques return oapi.MoveMouse200Response{}, nil } +func (s *ApiService) doMoveMouseSmooth(ctx context.Context, log *slog.Logger, body oapi.MoveMouseRequest) error { + fromX, fromY, err := s.getMouseLocation(ctx) + if err != nil { + log.Error("failed to get mouse location for smooth move", "error", err) + return &executionError{msg: "failed to get current mouse position: " + err.Error()} + } + + // When duration_sec is specified, compute the number of trajectory points + // to achieve that duration at a ~10ms step delay (human-like event frequency). + // Otherwise let the library auto-compute from path length. + const defaultStepDelayMs = 10 + var opts *mousetrajectory.Options + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 { + durationMs := int(*body.DurationSec * 1000) + targetPoints := durationMs / defaultStepDelayMs + if targetPoints < mousetrajectory.MinPoints { + targetPoints = mousetrajectory.MinPoints + } + if targetPoints > mousetrajectory.MaxPoints { + targetPoints = mousetrajectory.MaxPoints + } + opts = &mousetrajectory.Options{MaxPoints: targetPoints} + } + + traj := mousetrajectory.NewHumanizeMouseTrajectoryWithOptions( + float64(fromX), float64(fromY), float64(body.X), float64(body.Y), opts) + points := traj.GetPointsInt() + if len(points) < 2 { + return s.doMoveMouseInstant(ctx, log, body) + } + + // Compute per-step delay to achieve the target duration. + numSteps := len(points) - 1 + stepDelayMs := defaultStepDelayMs + if body.DurationSec != nil && *body.DurationSec >= 0.05 && *body.DurationSec <= 5 && numSteps > 0 { + durationMs := int(*body.DurationSec * 1000) + stepDelayMs = durationMs / numSteps + if stepDelayMs < 3 { + stepDelayMs = 3 + } + } + + // Hold modifiers + if body.HoldKeys != nil { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keydown", key) + } + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool keydown failed", "err", err, "output", string(output)) + return &executionError{msg: "failed to hold modifier keys"} + } + defer func() { + args := []string{} + for _, key := range *body.HoldKeys { + args = append(args, "keyup", key) + } + // Use background context for cleanup so keys are released even on cancellation. + _, _ = defaultXdoTool.Run(context.Background(), args...) + }() + } + + // Move along Bezier path: mousemove_relative for each step with delay + for i := 1; i < len(points); i++ { + select { + case <-ctx.Done(): + return &executionError{msg: "mouse movement cancelled"} + default: + } + + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + if dx == 0 && dy == 0 { + continue + } + args := []string{"mousemove_relative", "--", strconv.Itoa(dx), strconv.Itoa(dy)} + if output, err := defaultXdoTool.Run(ctx, args...); err != nil { + log.Error("xdotool mousemove_relative failed", "err", err, "output", string(output), "step", i) + return &executionError{msg: "failed during smooth mouse movement"} + } + jitter := stepDelayMs + if stepDelayMs > 3 { + jitter = stepDelayMs + rand.Intn(5) - 2 + if jitter < 3 { + jitter = 3 + } + } + if err := sleepWithContext(ctx, time.Duration(jitter)*time.Millisecond); err != nil { + return &executionError{msg: "mouse movement cancelled"} + } + } + + log.Info("executed smooth mouse movement", "points", len(points)) + return nil +} + +// getMouseLocation returns the current cursor position via xdotool getmouselocation --shell. +func (s *ApiService) getMouseLocation(ctx context.Context) (x, y int, err error) { + output, err := defaultXdoTool.Run(ctx, "getmouselocation", "--shell") + if err != nil { + return 0, 0, fmt.Errorf("xdotool getmouselocation failed: %w (output: %s)", err, string(output)) + } + return parseMousePosition(string(output)) +} + func (s *ApiService) doClickMouse(ctx context.Context, body oapi.ClickMouseRequest) error { log := logger.FromContext(ctx) diff --git a/server/lib/mousetrajectory/mousetrajectory.go b/server/lib/mousetrajectory/mousetrajectory.go new file mode 100644 index 00000000..5bdc81b0 --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory.go @@ -0,0 +1,239 @@ +package mousetrajectory + +import ( + "math" + "math/rand" +) + +// HumanizeMouseTrajectory generates human-like mouse movement points from (fromX, fromY) +// to (toX, toY) using Bezier curves with randomized control points, distortion, and easing. +// +// Ported from Camoufox MouseTrajectories.hpp, which was adapted from: +// https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py +type HumanizeMouseTrajectory struct { + fromX, fromY float64 + toX, toY float64 + points [][2]float64 + rng *rand.Rand +} + +// Options configures trajectory generation. +type Options struct { + // MaxPoints overrides the auto-computed point count. 0 = auto. Range 5-80. + MaxPoints int +} + +// NewHumanizeMouseTrajectory creates a trajectory from (fromX, fromY) to (toX, toY). +// Uses the default entropy source for randomization. +func NewHumanizeMouseTrajectory(fromX, fromY, toX, toY float64) *HumanizeMouseTrajectory { + return NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY, nil) +} + +// NewHumanizeMouseTrajectoryWithOptions creates a trajectory with optional overrides. +func NewHumanizeMouseTrajectoryWithOptions(fromX, fromY, toX, toY float64, opts *Options) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(rand.Int63())), + } + t.generateCurve(opts) + return t +} + +// NewHumanizeMouseTrajectoryWithSeed creates a trajectory with a fixed seed (for tests). +func NewHumanizeMouseTrajectoryWithSeed(fromX, fromY, toX, toY float64, seed int64) *HumanizeMouseTrajectory { + t := &HumanizeMouseTrajectory{ + fromX: fromX, fromY: fromY, + toX: toX, toY: toY, + rng: rand.New(rand.NewSource(seed)), + } + t.generateCurve(nil) + return t +} + +// GetPoints returns the trajectory as a slice of [x, y] pairs (floats, caller rounds). +func (t *HumanizeMouseTrajectory) GetPoints() [][2]float64 { + return t.points +} + +// GetPointsInt returns the trajectory as integer coordinates suitable for xdotool. +func (t *HumanizeMouseTrajectory) GetPointsInt() [][2]int { + out := make([][2]int, len(t.points)) + for i, p := range t.points { + out[i][0] = int(math.Round(p[0])) + out[i][1] = int(math.Round(p[1])) + } + return out +} + +const ( + // Bounds padding for Bezier control point region (pixels beyond start/end). + boundsPadding = 80 + // Number of internal knots for the Bezier curve (more = curvier). + knotsCount = 2 + // Distortion parameters for human-like jitter: mean, stdev, frequency. + distortionMean = 1.0 + distortionStDev = 1.0 + distortionFreq = 0.5 +) + +const ( + defaultMaxPoints = 150 // Upper bound for auto-computed point count + defaultMinPoints = 0 // Lower bound for auto-computed point count (before clamp to MinPoints) + pathLengthScale = 20 // Multiplier for path-length-based point count + // MinPoints is the minimum number of trajectory points. + MinPoints = 5 + // MaxPoints is the maximum number of trajectory points. + MaxPoints = 80 +) + +func (t *HumanizeMouseTrajectory) generateCurve(opts *Options) { + left := math.Min(t.fromX, t.toX) - boundsPadding + right := math.Max(t.fromX, t.toX) + boundsPadding + down := math.Min(t.fromY, t.toY) - boundsPadding + up := math.Max(t.fromY, t.toY) + boundsPadding + + knots := t.generateInternalKnots(left, right, down, up, knotsCount) + curvePoints := t.generatePoints(knots) + curvePoints = t.distortPoints(curvePoints, distortionMean, distortionStDev, distortionFreq) + t.points = t.tweenPoints(curvePoints, opts) +} + +func (t *HumanizeMouseTrajectory) generateInternalKnots(l, r, d, u float64, knotsCount int) [][2]float64 { + knotsX := t.randomChoiceDoubles(l, r, knotsCount) + knotsY := t.randomChoiceDoubles(d, u, knotsCount) + knots := make([][2]float64, knotsCount) + for i := 0; i < knotsCount; i++ { + knots[i] = [2]float64{knotsX[i], knotsY[i]} + } + return knots +} + +func (t *HumanizeMouseTrajectory) randomChoiceDoubles(min, max float64, size int) []float64 { + out := make([]float64, size) + for i := 0; i < size; i++ { + out[i] = min + t.rng.Float64()*(max-min) + } + return out +} + +func factorial(n int) int64 { + if n < 0 { + return -1 + } + result := int64(1) + for i := 2; i <= n; i++ { + result *= int64(i) + } + return result +} + +func binomial(n, k int) float64 { + return float64(factorial(n)) / (float64(factorial(k)) * float64(factorial(n-k))) +} + +func bernsteinPolynomialPoint(x float64, i, n int) float64 { + return binomial(n, i) * math.Pow(x, float64(i)) * math.Pow(1-x, float64(n-i)) +} + +func bernsteinPolynomial(points [][2]float64, t float64) [2]float64 { + n := len(points) - 1 + var x, y float64 + for i := 0; i <= n; i++ { + bern := bernsteinPolynomialPoint(t, i, n) + x += points[i][0] * bern + y += points[i][1] * bern + } + return [2]float64{x, y} +} + +func (t *HumanizeMouseTrajectory) generatePoints(knots [][2]float64) [][2]float64 { + midPtsCnt := int(math.Max(math.Max(math.Abs(t.fromX-t.toX), math.Abs(t.fromY-t.toY)), 2)) + controlPoints := make([][2]float64, 0, len(knots)+2) + controlPoints = append(controlPoints, [2]float64{t.fromX, t.fromY}) + controlPoints = append(controlPoints, knots...) + controlPoints = append(controlPoints, [2]float64{t.toX, t.toY}) + + curvePoints := make([][2]float64, midPtsCnt) + for i := 0; i < midPtsCnt; i++ { + tt := float64(i) / float64(midPtsCnt-1) + curvePoints[i] = bernsteinPolynomial(controlPoints, tt) + } + return curvePoints +} + +func (t *HumanizeMouseTrajectory) distortPoints(points [][2]float64, distortionMean, distortionStDev, distortionFreq float64) [][2]float64 { + if len(points) < 3 { + return points + } + distorted := make([][2]float64, len(points)) + distorted[0] = points[0] + + for i := 1; i < len(points)-1; i++ { + x, y := points[i][0], points[i][1] + if t.rng.Float64() < distortionFreq { + delta := math.Round(normalDist(t.rng, distortionMean, distortionStDev)) + y += delta + } + distorted[i] = [2]float64{x, y} + } + distorted[len(points)-1] = points[len(points)-1] + return distorted +} + +func normalDist(rng *rand.Rand, mean, stdDev float64) float64 { + // Box-Muller transform + u1 := rng.Float64() + u2 := rng.Float64() + if u1 <= 0 { + u1 = 1e-10 + } + return mean + stdDev*math.Sqrt(-2*math.Log(u1))*math.Cos(2*math.Pi*u2) +} + +func (t *HumanizeMouseTrajectory) easeOutQuad(n float64) float64 { + return -n * (n - 2) +} + +func (t *HumanizeMouseTrajectory) tweenPoints(points [][2]float64, opts *Options) [][2]float64 { + var totalLength float64 + for i := 1; i < len(points); i++ { + dx := points[i][0] - points[i-1][0] + dy := points[i][1] - points[i-1][1] + totalLength += math.Sqrt(dx*dx + dy*dy) + } + + targetPoints := int(math.Min( + float64(defaultMaxPoints), + math.Max(float64(defaultMinPoints+2), math.Pow(totalLength, 0.25)*pathLengthScale))) + + if opts != nil && opts.MaxPoints > 0 { + maxPts := opts.MaxPoints + if maxPts < MinPoints { + maxPts = MinPoints + } + if maxPts > MaxPoints { + maxPts = MaxPoints + } + targetPoints = maxPts + } + + if targetPoints < 2 { + targetPoints = 2 + } + + res := make([][2]float64, targetPoints) + for i := 0; i < targetPoints; i++ { + tt := float64(i) / float64(targetPoints-1) + easedT := t.easeOutQuad(tt) + idx := int(easedT * float64(len(points)-1)) + if idx < 0 { + idx = 0 + } + if idx >= len(points) { + idx = len(points) - 1 + } + res[i] = points[idx] + } + return res +} diff --git a/server/lib/mousetrajectory/mousetrajectory_test.go b/server/lib/mousetrajectory/mousetrajectory_test.go new file mode 100644 index 00000000..e2915b48 --- /dev/null +++ b/server/lib/mousetrajectory/mousetrajectory_test.go @@ -0,0 +1,87 @@ +package mousetrajectory + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHumanizeMouseTrajectory_DeterministicWithSeed(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points1 := traj.GetPointsInt() + + traj2 := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 100, 42) + points2 := traj2.GetPointsInt() + + require.Len(t, points1, len(points2)) + for i := range points1 { + assert.Equal(t, points1[i], points2[i], "point %d should match", i) + } +} + +func TestHumanizeMouseTrajectory_StartAndEnd(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(50, 50, 200, 150, 123) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "should have at least 2 points") + assert.Equal(t, 50, points[0][0]) + assert.Equal(t, 50, points[0][1]) + assert.Equal(t, 200, points[len(points)-1][0]) + assert.Equal(t, 150, points[len(points)-1][1]) +} + +func TestHumanizeMouseTrajectory_WithStepsOverride(t *testing.T) { + opts := &Options{MaxPoints: 15} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, 15, "should have exactly 15 points when MaxPoints=15") +} + +func TestHumanizeMouseTrajectory_ZeroLengthPath(t *testing.T) { + // Same start and end: should produce at least 2 points, both at (0,0) + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 0, 0, 42) + points := traj.GetPointsInt() + + require.GreaterOrEqual(t, len(points), 2, "zero-length path should have at least 2 points") + assert.Equal(t, 0, points[0][0]) + assert.Equal(t, 0, points[0][1]) + assert.Equal(t, 0, points[len(points)-1][0]) + assert.Equal(t, 0, points[len(points)-1][1]) +} + +func TestHumanizeMouseTrajectory_MaxPointsClampedToMin(t *testing.T) { + // MaxPoints below MinPoints should be clamped up to MinPoints + opts := &Options{MaxPoints: 2} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MinPoints, "MaxPoints below MinPoints should clamp to MinPoints") +} + +func TestHumanizeMouseTrajectory_MaxPointsClampedToMax(t *testing.T) { + // MaxPoints above MaxPoints should be clamped down to MaxPoints + opts := &Options{MaxPoints: 200} + traj := NewHumanizeMouseTrajectoryWithOptions(0, 0, 100, 100, opts) + points := traj.GetPointsInt() + + assert.Len(t, points, MaxPoints, "MaxPoints above MaxPoints should clamp to MaxPoints") +} + +func TestHumanizeMouseTrajectory_CurvedPath(t *testing.T) { + traj := NewHumanizeMouseTrajectoryWithSeed(0, 0, 100, 0, 999) + points := traj.GetPointsInt() + + // For a horizontal move, the Bezier adds control points, so the path may curve + // Middle points should not all lie exactly on the line y=0 (curved path) + require.GreaterOrEqual(t, len(points), 3) + allOnLine := true + for i := 1; i < len(points)-1; i++ { + if points[i][1] != 0 { + allOnLine = false + break + } + } + assert.False(t, allOnLine, "path should be curved, not a straight line") +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index d4d2712b..686fbc48 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -322,9 +322,15 @@ type MousePositionResponse struct { // MoveMouseRequest defines model for MoveMouseRequest. type MoveMouseRequest struct { + // DurationSec Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. + DurationSec *float32 `json:"duration_sec,omitempty"` + // HoldKeys Modifier keys to hold during the move HoldKeys *[]string `json:"hold_keys,omitempty"` + // Smooth Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + Smooth *bool `json:"smooth,omitempty"` + // X X coordinate to move the cursor to X int `json:"x"` @@ -12298,144 +12304,147 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9eXMbN/bgV0H1TpWtHV7ykdl4/lJsOdEmTlSWsplJ6OWA3Y8kfuoGegA0Jdrl+exb", - "eEDfaDZJSZaV/VWlYorE/Q68G5+CUCSp4MC1Cl59CiSoVHAF+Md3NHoP/85A6VMphTRfhYJr4Np8pGka", - "s5BqJvj4v5Tg5jsVriCh5tNfJCyCV8H/GJfjj+2vamxH+/z58yCIQIWSpWaQ4JWZkLgZg8+D4LXgi5iF", - "X2r2fDoz9RnXIDmNv9DU+XTkAuQaJHENB8HPQr8VGY++0Dp+FprgfIH5zTW3qKDD1WuRpJkGeRKa5jmg", - "zEqiiJmvaHwuRQpSM4NACxoraM5wQuZmKCIWJHTDEYrjKaIFgRsIMw1EmcG5ZjSON6NgEKSVcT8FroP5", - "WB/9FxmBhIjETGkzRXvkETnFD0xworRIFRGc6BWQBZNKEzAnYyZkGhLVd471AzHwShg/sz2PB4HepBC8", - "CqiUdIMHKuHfGZMQBa/+KPbwoWgn5v8FFvtexyy8eicyBbsecv185pnWFh/qx4NDEvurORNm0I6Gmlwz", - "vQoGAfAsMWuLYaGDQSDZcmX+TVgUxRAMgjkNr4JBsBDymsqosnSlJeNLs/TQLH1mv25Of7lJAQFv2jjY", - "VGaNxLX5M0sDN4x3gpWIo9kVbJRvexFbMJDE/Gz2Z9qSKDNdEcZ21ApwW6PXQTYIeJbMsJebbkGzWCNw", - "G4STJXOQZnOaJYCTS0iB6tq8bnRz7EtA+r5p7+IfJBRCRoxTjadVDEBSoZg7s/ZIm/ZI/zxkpAaa3gRm", - "aC+S1pF/XzagGF/G0GQCVR5AFUmptHRsucaIXK6A/Mss5V9kwSCOiIIYQq3I9YqFqykvR0lBLoRMBoTy", - "yO5cSHu7RQYdbG/DTSkzDGIF+QpSKmkCGqQaTfnpDQ11vCGCF7/bnolZT45XZkEkyZQmcyCpFGsWQTSa", - "8hbjstSRGDLs5S0tHmC4taTL3bq/kXTZ7J2INezW+51YQ7N3KkEpQ3l9nc9Nwx9hU+mrQiniuK/jBbaq", - "dgM9CzOp7NW3tSvo19iw2jsGSHs7mkYl/+5gXDmMiyulgmGjCgurwrd23nbkmYYbHVSPsjiaGmxrO883", - "4mOG5aA92zSs9xJudHE8DTLHkb1ULoFqeMMkhFrIzWH3USIiz6n+ktruJMpHJ6YheSpCTWNidzkgMFqO", - "yN9evjwakTeW/yJ7/dvLlygYUG1Ep+BV8H//mAz/9uHT88GLz38JPGeVUr1qL+JkrkRsuE25CNPQzBDi", - "1huTjEf/sz144zBxJt9hvoEYNJxTvTrsHHu2kC88wmnufuHvIcTrZHnY6lnUXvtZZKQ8vLTdBSXzSSo7", - "ISdxuqI8S0CykAhJVpt0BbwJfzr8eDL8fTL8dvjhr3/xbra9MabSmG6M6M+We+5nBSgftfb0OpMSuCaR", - "HZvYdoRxkrIbiJX3+pawkKBWM0k19A/pWhPT2gz8w0fyNKEbc/3wLI4JWxAuNIlAQ6jpPIYj76TXLPIh", - "VHM2bLZ1/d6jbd5A9yPDGrbZIb8WcqsVZH0MNIKYbmqi3aQpqrwxTczuExbHTEEoeKTIHPQ1AM8XYmRX", - "lDSUplI77DX8n9BYOCnBUNcIl8VZYhY68cHkNvKtOYu9xFs/Q2lqUX/cDMjmQ1WYTCmTqtiiXkmRLVdG", - "BovtIpaML0fknZGInIhFqCYxUKXJM5IKxrWqaVnNJVcOJKE3TqV6VtWvnrV3s/VHpSGdIbhnSV2Yf7kn", - "yCXEVLM1EDOkauyaPDWEZ4DBODP6K8E5j/oBj6PNUpAzBcvE6filxjHpVjmKBSE07KpSkMSNYzZS4B95", - "ZxdBjmsrOu5VBDrvhsI00bjzQSm6BA8aNgbOG3rHtqrAeUw310jEhxkdXK+qclEOSUIjnbQkda/IYsSo", - "C/x7/L/pmtqPOEDNxHCJ6kYEZEUVoWEIConlSUqX8GRAnqAR50Y/scrJk7kU1wrkE7Kmkhlu7TSPJI3h", - "FZkG9JoyTUzn0VJo8fTJSutUvRqPwbYZhSJ5cvR3IkFnkpNKc810DE+P/j4NptwnExllVWR6piCsYds3", - "LWx7R28QbaCwnpi+BrUceRTSGWGKfDNB7LJ9glfPJ5O9cA0Pf0d8ULjgPdHBdDKU08CCcnctfIAcy+tD", - "IfITh8Lm2i3PZ0FZDJHv1GWx6LaasaZxBg6SEJH5xumuRi5mC0L55sgyiwikZz0XmvKIysgasshCigQH", - "qG6stR6lI5HpLYOJTKeZ3nW0DBG+PdxvK9ArkOWGHL1ExHVZZHG8KYecCxED5S3syCfwIchbFsMZX4g2", - "P2JqFjG5fVUoQDNFaKkNjDzrMZpdNDP43x7uJ3PFJXhRW9Ms0snIWs0SqoNXQUQ1DLG35/T8qpLZllWO", - "5kwr8tToRAMyDSJ5fSOH5r9pYOTiaTCU10M5NP9Ng6ORbwZOfev+jiog5qdcDl+YKYX0nsTOSlUu8rSR", - "hH2E2XyjwYMnF+wjMhb8eUQmZFFZBgM16rda4R7d6mqTDXI8qMDQHXoXOl1slIbkdF3cyE3AKGxAwhXl", - "SyBgGrZN1rugH10sIDT0sDMeHgrLYqpDgboflvitKnikaFepmlBevz89uTwNBsFv78/w3zenP53ih/en", - "P5+8O/WI8T5bxqBbYPmJKY1w8+zRSItmb+0TY9wSsCFp4DpHxJ38BAVX8ojgP4llB26dkFgsca5NyXor", - "Tp82klVkrgZXEsvikjKSx6hLGFCaJqnnZjJ3vZm+XNE1VSSVIspCi0W7sLcOya86tQ9gqPKdO5P1e+eh", - "bHP4XW3puVntcBt61wg7285b9tU9LQ+30BGNjrCXjth3rKUWmJ8M0eKg4911pL2O+XBrWwRKz/qshqC0", - "Wbx1HNjLrs/oNgiUDPsGViKTIew8ZlNEyicYVHbhO6Ffrqr0tIcM/T1wNMb98iPJYwba/Ehc1bQKLTNo", - "e74jw85A5ULgqF8AFFfevZxTHa6cQe9Auuqw6L3ptuQVWs2zF5P97XpvOu15I3K2ICJhWkM0IJkC66Na", - "seXKaLJ0TVlsVEXbxUhI1niK6OMuB3elfjMZPJ8Mnr0cHE8++JeIRztjUQz98FoQ/NosOVNgHZ1GwCLX", - "K+AkZmsgawbX5vIsTLljCbhNI9KEmq3BL81IQOvZLFxJkTCz9k/ds2NT8to1JXShQVb2n4tjRi3nKpNA", - "mCY0oqn1HnC4JmbVNa0VcQLPcgU0WmTxAGcrvok70LPTkPqm04BaoM3zZ5PdzKlNr9qevCyTNHfTbjF1", - "ulbFvWFwCi8StG82DGJVFDXgngxsWyqBaJqmVi442NpZuIeSvivtCjYEXWoubCSE0V43nH/+n5z104yu", - "NslcxDg5TjQipzRcETMFUSuRxRGZA6GVtkRlaSqktjr8TSS0EPGUP1UA5B/Hx7iXTUIiWKCdUHB1NCLO", - "5qMI42GcRUCmwXu0BEwDo+1drNhC24+vtYztp5PYffX25TQYTa0F1Jr8mLIm3BAXSGMlzCpDkczdlaWc", - "d82O91edK5H4F87210s6x2H3ONAGt8bT9fJrKQzDP72B8M7MetRsL0FD/IYbPsJFprwhRHJZtwL/8aEd", - "D2ZHonKZJdC0WPdiFVUzKUTdiuvfRubss/Y80JlBTFeSSrZmMSyhg+1QNcsUeLTK5pBUWXQwrc1QPIvx", - "9sh5fDuMx+7do7ThQePNIyRRK4jj4sjNXZBxr24RXnvG+k3IK0PDpZL1lFaVzCM3orMY2UkY922gX+YC", - "vu5Gr08+z5CD2adWlNwpXzMpONrWC5OtWasCXVzF7ugrp1Fifsvsup+ltRuA3QZVC85eMryVNZVWia4A", - "WLGPNhHmt1LhkWljmtl/3qx1AXm1DLhheuY337utEtMETZD+EaxxdTb/5oXftvLNiyFw0z0itimZZ4uF", - "pawO4+qug4lMdw/2uRt6P7IycGY/8F2wpblkEXstDTewtw4yhc1rTC24PH3/Ltg+btXC45r/ePbTT8Eg", - "OPv5MhgEP/x63m/YcXNvQeL3KIoeepugGEvJ+eU/h3MaXkHUfQyhiD0o+zNcEw0yYWbnoYizhKs+N9sg", - "kOK6byzTZE9/HY46sAvdcmIXKb2uhfLG8S+L4NUffSFerav786Bpj6FxLIxqN9N6038LnrjWhJJUQRaJ", - "YbH7p+eX/zxqMlYr2eNFlIexok/W3Egd16UfaGfOT9sEnFVoqpswOoJht4eCtDWTaXb4NG128KEF1wP4", - "+VnF0EnnhiFRosxo2+gh9QX3/HJRAOvsjZ/Vut9nvu42Fn5IlaF7iAgrY4U8l2xhf8wyFvkZMTXi+Ixq", - "v30T7Y8WGlU0c932MHF2kpqmOlN7QiOPxVHY2d6y3VwpzWZp6NnfqdIsoUYZeX3+K8nQDpyCDIFruqze", - "ghyDCnqu0dP8+iRsUTurFbV3qz2uPhllECSQdDmByhVLUAh5kkBiZES7+sI/1HGDe80t5yVMdc3pIDPO", - "DfjstiHy30XdgI3YgekQb6imhpNdS2YNoA3Us/5XxtPM41OKqKY7CRZRdZZRr/WwGPdD755vJS+a5bhQ", - "KWWGa+/QtNDAu5CkDIHBBsQ1HwW7mlTcViTQ0sG3j+x0cUpSuokFNWiaSlCGQ/FlAUHnOBeSxGwB4SaM", - "nYNQ3RaahUOoRBazC68ICn7/0k/1JbU8cYYUvEFzO7GGgpHawZkiU+w4DbpI1qzfcwtYQ7j9OffA4BGE", - "q4xfVRfs4hiK6IjdiNhGtYL0hw0sGGdqtdu1UYau5r26Lo1e/dveh+2vVRGDW/m9IuLsccmVq3WdDlxs", - "g3ng5Vtdp4+JXIQSgKuV0O9huUv2yG52+h+sfb6IJF46pXFL3G2H5fY3tNjuM9CO3kc71hMjvqbDGBaG", - "WiSHW/kj9xjT6zrLT2GQH2wfyA6xQMsC0D0pIHXE8JJsPVFkX69erOnsZrsh/Ach2UfBMQ0B5yI0ERnX", - "I2Ld0EbRwO8VweixAeGwpLXvDRz8nM6uoCfq+P+YFYc7zB+Ja+6ZPkv9k9/GdVykqtyd85hqm7lVyaep", - "T7U/Uew95M7u5FaS0Z5ci0UR8J64OOv2Ln0KrlOvT9S161j2WxbDudE6lWKCq8PWv5QiS/2GCvzJhRxJ", - "8n1N29s3ts2T/fPNixdH+yX7iGvus4ubteJPaAnP1/trx3p3iYO6XgmFulR+ttb9ZT0t6IKMDk3E2RKX", - "Vs1a209kPaeZgmqUqpCo30NoaD8qbK17GmurnkNMV/PZaqvxwLWI4EkvUVYn9x6IEWHeqt+oDu80t6pI", - "fEP1CXNQ/RG9hnDZGvrtXAW1u/FI0Tfe7BD70BnJgSdwywythaQJ+CMV3peybd7IgHiRGopdg5QsAkWU", - "LV/gTuCoCvNnkz6jmdeElDuBPcafigALSHt3lCeGi84R+oxfWATudtSU66g6KvKske2ns/VAEnqDAajs", - "I5zxd991rwCjFZULm3333Y4QOW5Q4fGOkQgXWqS3RTQhQzDj9NPLWZJAxKiGeIMFG9A9KjJNlpKGsMhi", - "olaZNlLQiFyumCIJxtOgjYFxdAhLmaUaIrJmEQg8LL99eJ8ERUvBZkH3mJ3YzNrdW9K9XW6bkQO1FFeg", - "euM48hzkhsYJN+idt7nT1hywEhiRYLP+ey9CHLfN7kwz5vR1zHEJXgU/guQQk7OELkGRk/OzYBCsQSq7", - "lMnoeDRBySAFTlMWvAqejyaj5y5QGA9snAcejRcxXea3Qui5Ft6BXAIGEWFL67KHG6bQ+iM4qAHJUqNE", - "k8agntClNaNEZSnINVNCRoMppzwimMSTcc1iPLai9RtYXwoRKzINYqY0cMaX0wADc2PGgTBFxByp3siP", - "CyHzbBJklC7GDuM5DK5YHhehYKDDVT7LW9y/BQUo/Z2INnsVn2lQe36aDdN2viV7hlqQBI/VZTf8MQ2G", - "wysm1JWNbxkOI6boPIbhMs2mwYejw0NS7IL8aFW20zIDG5VWlkR6Npl4JFhcv4V3hCldxdYcsJs5Lp8H", - "wQs7kk8ZLmYcNyswfR4EL3fpVy9fhLV8siShchO8Cn61eFksMaYZD1cOCGbxbs3YrcTeLI0FjYZwo4Gj", - "oDukPBrmbQ3MhfKwgF+xG9bPEJIkBh2LIchHlhIqwxVbG4KBG43FaPQKEpJxw2LHK5HA+Aope1xOPZ5m", - "k8nz0Mjv+AkGU65AE2noJanOYHfF+AFkSHIqnPIvSIb2vE6LrZ7w6L07423kmGSxZimVemz03WFENd1G", - "keVRdse9lW0MaVrw45mgp9UIiRX6qw/vT0t5K2IDU9S6jG4e0xBcOlkOrv2g3rhgT4a/0+HHyfDb0Wz4", - "4dPx4NnLl37l8CNLZ0YKaC/x9xIh88RlAy9qVpbakIACA8pVP8XSL3nMXkI5W4DSI8MWj6pG1TnjhgT7", - "7rxieS6/xyftb2VvFegexuOOfYb9AhssKkA08LA5SzUFcTBFJNDooRleiwUV0Kwg+VOqDENSR1UmWGzR", - "cUMnt4znuVzg53qneTgiJ6KRTN+qeoZCqquGdHJ+RkIaxyNy4n6lEnIrFkSGy5V10Vzm+UrEkUNSuAnj", - "zKiSJBbh1YAoQbggAvVN9CGSgtkoElJuIydioGvAjOO+wmhFLaX84AkrwvetzS2vkYS5r6MpR4ncBh4a", - "Ud2obuHKUVUENhDCSE1hEbqLPm4sg4OzXcFmLqiM8uOa8lz+T+nGjMJBXwt5RaTIeDTUkqUkphp4uMHZ", - "AON0ecTWLMpo7IbxcV5PibtbSEDbjNxbiukdKoKcxHGBUP602oekwIIcthT/q2J2g9gaVbNykquDr6yX", - "dU9Q8xTkOhBYtoRJXm4sJ+4HhdAFS7LYRl9Z2qvW6POrbg0YFWW6/OApbOf3BJ12AbCdgXMn81cSqXyl", - "Ra1Zf80Um7OY6U2hLXw1NPoDi1xIt7iuZjnWwVwvQOe//DBTBZk3OpByjLKVcgZEOJOeucCMjuss0Csh", - "ta2VMjDT82b1nCVbg82xc/dzDFQBXjHVxP2emjU+xl8UKron1GyX4juQb5iBvhJ+gUux6aTIyxBMFOHQ", - "wJglaIsws6LoZCeT+B50Ldc3uEeC9ScV+2kXI/DsTotN3MUpfg86J7XKFM4HmM+0C/etV3b0H26Rc3xP", - "aN6uGXmr69GdgtnZw6L6uzwluQYdF6pXOs5KTqN2gVitmuYWPuryPst50DmPPJMXrLT02pEfzc+l+7iS", - "vDblvpS0EXmL/NcsTMLKKENGfWjnvg2IAphysxh//hqhmuSFicIl06OFBIhAXWmRjoRcjm/M/1IptBjf", - "HB/bD2lMGR/bwSJYjFaWnzuvxUpwIVXVOD2MYQ3lfo1i4XxSoTsK9D4qZ0mwUBCR1+DpEirviRxaVVAP", - "pAYEKGLL1yQt2Du+qlIjXu6A+KqI8OlmVZf0CspIoPuSGFsBTZ8djLbeOCyhSxinNgCvnKnfyNO6WMoF", - "EBz0QQH6mqY6k0b+LwGUe7x6wOkq+/qZmA3VImsXzhRvjPQ2Foa28xAr852uyHgVTlqXFmvmjlpWsBMD", - "a7FS1nbCOInFEiOpNAuvlK3PZ+P4rKWngkFkDiu6Zgal6Yasqdz8negMjRWuumZOwKMp/80IqXOhV5Wt", - "4ID5XgkGejkTjisKPbDc3LI3nNky+KSm/5KnxRgoCpcTHFnnEKrRaHQBiF1EsWOF/3KM3Wlww6GrQ/4z", - "GQ5RvCYTYg2pViC3ptR/+TjkRR4xdU/kVy32fCB3dOj1lSjRdjGlrGDBQ7WRjPeQ5vJySR3M0TmF7wku", - "7UrRh0HG+n436dd0a+EDBNosrBsKruhtzfnr8ZS60g73JTx4Spl8YYNGvTKy5/r61Vkw8irBIbbM60zc", - "AswvJt/296u/DXOHftGO7RjUWKixrQk+KzLWEU0ynzmyXjf9vmyS/urshzp5ymg3u8+viHTtTgnFoIvy", - "+HO42ELhO8DFVjK/b7i0C70fbPMpQGK3GN2Osl7096s/OXQnxiJcebWaYRNuuTd2C8jeWo/o1w0tjGX+", - "EwAK4VHASFzzWNDIUNfsI8OYvSVoX4yoziRXhJLfz85tUGLFiW6LeCC4VK5ZVOKOqwUkG/B3879h8neW", - "otM/f6EFE9V3ftAh9+wbCTrfFNZ0Mf3+nQGyAxu7kEdg13FgUA2o6Ivo/rDX5ezO9VYKpTn1fI9FsCIi", - "VvWAHyNeOmBVWQihOaK5LXfgq9LRDgirqRx9VJo81VRWIkCS3PCCAX5mrKOteD3lWxCb/K50RMRiAVIR", - "xZYcawRzHW/IgioNspgQU+95NOURVL8yn6kELNLxkaVOIabhisHarGQOujkKkpHf61GhKnNGj4WsBp/a", - "ZZqK7aJ1cER+YMsVSPtXUaWUqITGMRTgVWSeaaLpFZBY8CXI0ZQPLSSUfkX+Y6BthyDHA+Kipg1gISJP", - "//N8Mhm+nEzIu+/G6sh0dEG29Y7PB2ROY8pDI0qZnmOEAHn6n+OXlb4WcPWufxvk8My7vJwM/1etU2uZ", - "xwP8tujxbDJ8UfTogEgFW2Y4TFAFR1nkJf9Uptu6owoGld/skvGD8iUP78sVHfXeii1eOtr+/4w16vq2", - "C/Zo+NcsD552bLHOGopyxbvyhN6K0F/DDbufTFiWbG4jFEp5lXrQjxBtvgddq2idF3ppQa9Am5gpjXK6", - "6sSbsrD2YZfJ48SUctceVCnVt9gmBzxCXMGAYIS8jVVs4wbWlO5S3/IizPfodr4L1Q3dvKW54xHCCXeA", - "ZXcxxHobMUugUaF0e2n5PdDIqdy7kTJOlouEZvyvhZpFqEEPy/Iit5IlkPWb3d2ZZeyBkMXAt1Rl8Pnv", - "HDkUWEY/q2Q1d1J3O7n8/gL8OrLYD6X4ylB5ON4jBOQFaM9rFRXQjTHhXa1YWkDYRvB3O21P4lhc54H+", - "mLBiw9OFJDbRJAZ3IbgwGAmJcDzAvoYy6khsycWDO8tkKSSSjlSUQ4r4VwpyOYF2t7L+OUPdN+HDJXts", - "r9S/PaENT+HOkj0QSkWex2NndZ78j4WT16rkkJs2t+axUTS8IL3Z2rY2ZY1pVdo2W6FhvkcifMRhrZt3", - "Rhr7on5UrXVQScYrFGctdqODan7VLZKfttHDgYj9O0tLtK4A8E+D5LSaU9lA0Ra+O+NKD8Lvaxrtoosp", - "7yeMfhNpzSI65Q2TaHdGpbNx3hlx5VYV/1vhDYtTfoX0EsPg4YjWfEpnJd5tLxxQVl6MwYoIeHGW3W11", - "BMnSvICUWxvmS8bsCg+JDIfYZlj2630otcEvcjjcC7s4cWf4J2cZTXTtYBvXzZzHhiZQKcFzXzqAp8rP", - "7rA9sHoBbttboPhXzv6dga80TUmV1+44eqt9tHVN3Ca56/oBD4RsdjNVI7XLBeXLiiSGpzX+lB/5Z1fG", - "BGxFoia+ibREt4aRAg0PztLg7A4FHLfZHvpNDZ76rDmgRJo+fkBdYI0dsyNMKvYYj5pAGtv4005Tkq2v", - "+1ad2mZfEFZNs5CGG21X67UH9fkDqi+B+uK5L04rZWpLXdjF52J5TRrhrj8F/xheXJwOX9u1DS+9D2S+", - "g4hRVzxnQczwWPfWhfs+bTKxo5rnLvfStVidxyn3+TGiKR5065RdOqFluwXGGmV+e5DRb6bJLgbPNxXh", - "i7aMn1/Q712URlsUBRQ7ayfmr5yhWPbNixddy0zsU+jeZW2tuGiJb5cb/5bm2AOtGXlp8Ed/jaJZytyc", - "eTxkGaoVi6Ualwfrd9GJpat33sGHGwhhn6Hcirk5o8kfWy5K6Hjrb/unWYg4Ftf+yINa0elKWcQmmAWP", - "N0V+BmGL/AlNpohb2hbC7L5V9pmnsnf/bGWDmavbHjzYjVY8PNx7lRnE+qpvL9/NYBZNxBqkmdoSSFq8", - "9j92NTJ2qOAi50xLKjfkvOjt3r7ghvrw2c6ynCqC5kYTuqSMK6uJz6W4ViCJe2RiygUnsQhpvBJKv/r2", - "2bNnI3KZv4m/oorQMH/g5klKl/BkQJ64cZ/Y+jpP3JBPyufHXAaULB5XaLyyj1UplatBZfCW1wq5+Awn", - "7gjKfb+2t8N9aHatuR4o68GzDnziwpcXXh7u11hrpdwCpvRc4MotRniQ0xGI5UlIHd2KfuXxp3vLnW0/", - "L/Vl8aD9KJ4HA8qCSdK1+Spq7HhfwKwDGN9z6oUwviF1vyCuPT/2MDCuvpTluwrt01dfGWzpFuB+Kh/V", - "+jy+YvXsXC+gf2SY5tmvl1ee69omEva8xbW7snAQQKtvIX5VVYB++fFRxhcYVlI85piLrd0YZ58D78U5", - "+9zinwfr6k9P/jfe3T5AqfM5zi3Ip4o39rzqb/0lvi+Ne/d8j9lN+a4w98ujjFKuPIZnt9cN+ojtINNg", - "qz8N16k9PfhA8lPlJUAP8n1XfZnv0VrcypvPPlW4HQ9FpvsMceXhiUxvtcg9ED+6hWXJ865ir42p8WKi", - "kXGbTyb+twPlHhwoFawWmW4YzIqXTcalE9bPXW3mcPno330marfeHumu29T1hs2DpWg/UG2LIrE7lbBm", - "qDPm75hUn0VpQd0ll3VysTz7rAr4rd6zwmlVvKJSRk+MCJZUEom5KuqVkrK8Dp7zChTduxxZyPT8bqy+", - "d1j6WSMe2DhJX9w6naDyqpJ1PdYYXPHr8K17T3R4svVdT7Eon11tP0Y6It9nVFKuwcbLzYG8f/v6+fPn", - "3462e0BqS7mw8SgHrSR/S/vAhZilPJs820bYzHAyFsf4WKcUSwlKDUiKtWKJlhtr+8QK4bJ+3O9By83w", - "ZKF978ZdZMulzRXFkrX4yETljafygQe5sURQbmLrC+6fH3HCqS1zpZAWAUM0d+AoMbO3R2f+YP4ar7pt", - "7dciH2DbhVJ7+7cdZN+i1/xtDFms8s4S7GgcV4etH1vrkRVP6N19X77+B+a8d+/xNhLNXxt+fBWi8ASK", - "CoklXxuRX3i8wQSDktelIMnZG3xlYW6f6FUaH4LAcnCGg4zaUBbpNiBXnl27Nxh7nnbbX7xyoXAPW4xP", - "i7R+/eBG/l8AAAD//yIMPJrxsgAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index b0bc70d9..3d09aca3 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1232,6 +1232,15 @@ components: description: Modifier keys to hold during the move items: type: string + smooth: + type: boolean + description: Use human-like Bezier curve path instead of instant teleport (recommended for bot detection evasion) + default: true + duration_sec: + type: number + description: Target total duration in seconds for the mouse movement when smooth=true. Steps and per-step delay are auto-computed to achieve this duration. Ignored when smooth=false. Omit for automatic timing based on distance. + minimum: 0.05 + maximum: 5 additionalProperties: false ScreenshotRegion: type: object