mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-09 13:58:25 +02:00
f5e6f53d75
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
458 lines
10 KiB
Go
458 lines
10 KiB
Go
package rod
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/go-rod/rod/lib/input"
|
|
"github.com/go-rod/rod/lib/proto"
|
|
"github.com/go-rod/rod/lib/utils"
|
|
"github.com/ysmood/gson"
|
|
)
|
|
|
|
// Keyboard represents the keyboard on a page, it's always related the main frame.
|
|
type Keyboard struct {
|
|
sync.Mutex
|
|
|
|
page *Page
|
|
|
|
// pressed keys must be released before it can be pressed again
|
|
pressed map[input.Key]struct{}
|
|
}
|
|
|
|
func (p *Page) newKeyboard() *Page {
|
|
p.Keyboard = &Keyboard{page: p, pressed: map[input.Key]struct{}{}}
|
|
return p
|
|
}
|
|
|
|
func (k *Keyboard) getModifiers() int {
|
|
k.Lock()
|
|
defer k.Unlock()
|
|
return k.modifiers()
|
|
}
|
|
|
|
func (k *Keyboard) modifiers() int {
|
|
ms := 0
|
|
for key := range k.pressed {
|
|
ms |= key.Modifier()
|
|
}
|
|
return ms
|
|
}
|
|
|
|
// Press the key down.
|
|
// To input characters that are not on the keyboard, such as Chinese or Japanese, you should
|
|
// use method like [Page.InsertText].
|
|
func (k *Keyboard) Press(key input.Key) error {
|
|
defer k.page.tryTrace(TraceTypeInput, "press key: "+key.Info().Code)()
|
|
k.page.browser.trySlowMotion()
|
|
|
|
k.Lock()
|
|
defer k.Unlock()
|
|
|
|
k.pressed[key] = struct{}{}
|
|
|
|
return key.Encode(proto.InputDispatchKeyEventTypeKeyDown, k.modifiers()).Call(k.page)
|
|
}
|
|
|
|
// Release the key.
|
|
func (k *Keyboard) Release(key input.Key) error {
|
|
defer k.page.tryTrace(TraceTypeInput, "release key: "+key.Info().Code)()
|
|
|
|
k.Lock()
|
|
defer k.Unlock()
|
|
|
|
if _, has := k.pressed[key]; !has {
|
|
return nil
|
|
}
|
|
|
|
delete(k.pressed, key)
|
|
|
|
return key.Encode(proto.InputDispatchKeyEventTypeKeyUp, k.modifiers()).Call(k.page)
|
|
}
|
|
|
|
// Type releases the key after the press.
|
|
func (k *Keyboard) Type(keys ...input.Key) (err error) {
|
|
for _, key := range keys {
|
|
err = k.Press(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = k.Release(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// KeyActionType enum.
|
|
type KeyActionType int
|
|
|
|
// KeyActionTypes.
|
|
const (
|
|
KeyActionPress KeyActionType = iota
|
|
KeyActionRelease
|
|
KeyActionTypeKey
|
|
)
|
|
|
|
// KeyAction to perform.
|
|
type KeyAction struct {
|
|
Type KeyActionType
|
|
Key input.Key
|
|
}
|
|
|
|
// KeyActions to simulate.
|
|
type KeyActions struct {
|
|
keyboard *Keyboard
|
|
|
|
Actions []KeyAction
|
|
}
|
|
|
|
// KeyActions simulates the type actions on a physical keyboard.
|
|
// Useful when input shortcuts like ctrl+enter .
|
|
func (p *Page) KeyActions() *KeyActions {
|
|
return &KeyActions{keyboard: p.Keyboard}
|
|
}
|
|
|
|
// Press keys is guaranteed to have a release at the end of actions.
|
|
func (ka *KeyActions) Press(keys ...input.Key) *KeyActions {
|
|
for _, key := range keys {
|
|
ka.Actions = append(ka.Actions, KeyAction{KeyActionPress, key})
|
|
}
|
|
return ka
|
|
}
|
|
|
|
// Release keys.
|
|
func (ka *KeyActions) Release(keys ...input.Key) *KeyActions {
|
|
for _, key := range keys {
|
|
ka.Actions = append(ka.Actions, KeyAction{KeyActionRelease, key})
|
|
}
|
|
return ka
|
|
}
|
|
|
|
// Type will release the key immediately after the pressing.
|
|
func (ka *KeyActions) Type(keys ...input.Key) *KeyActions {
|
|
for _, key := range keys {
|
|
ka.Actions = append(ka.Actions, KeyAction{KeyActionTypeKey, key})
|
|
}
|
|
return ka
|
|
}
|
|
|
|
// Do the actions.
|
|
func (ka *KeyActions) Do() (err error) {
|
|
for _, a := range ka.balance() {
|
|
switch a.Type {
|
|
case KeyActionPress:
|
|
err = ka.keyboard.Press(a.Key)
|
|
case KeyActionRelease:
|
|
err = ka.keyboard.Release(a.Key)
|
|
case KeyActionTypeKey:
|
|
err = ka.keyboard.Type(a.Key)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Make sure there's at least one release after the presses, such as:
|
|
//
|
|
// p1,p2,p1,r1 => p1,p2,p1,r1,r2
|
|
func (ka *KeyActions) balance() []KeyAction {
|
|
actions := ka.Actions
|
|
|
|
h := map[input.Key]bool{}
|
|
for _, a := range actions {
|
|
switch a.Type {
|
|
case KeyActionPress:
|
|
h[a.Key] = true
|
|
case KeyActionRelease, KeyActionTypeKey:
|
|
h[a.Key] = false
|
|
}
|
|
}
|
|
|
|
for key, needRelease := range h {
|
|
if needRelease {
|
|
actions = append(actions, KeyAction{KeyActionRelease, key})
|
|
}
|
|
}
|
|
|
|
return actions
|
|
}
|
|
|
|
// InsertText is like pasting text into the page.
|
|
func (p *Page) InsertText(text string) error {
|
|
defer p.tryTrace(TraceTypeInput, "insert text "+text)()
|
|
p.browser.trySlowMotion()
|
|
|
|
err := proto.InputInsertText{Text: text}.Call(p)
|
|
return err
|
|
}
|
|
|
|
// Mouse represents the mouse on a page, it's always related the main frame.
|
|
type Mouse struct {
|
|
sync.Mutex
|
|
|
|
page *Page
|
|
|
|
id string // mouse svg dom element id
|
|
|
|
pos proto.Point
|
|
|
|
// the buttons is currently being pressed, reflects the press order
|
|
buttons []proto.InputMouseButton
|
|
}
|
|
|
|
func (p *Page) newMouse() *Page {
|
|
p.Mouse = &Mouse{page: p, id: utils.RandString(8)}
|
|
return p
|
|
}
|
|
|
|
// Position of current cursor.
|
|
func (m *Mouse) Position() proto.Point {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
return m.pos
|
|
}
|
|
|
|
// MoveTo the absolute position.
|
|
func (m *Mouse) MoveTo(p proto.Point) error {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
button, buttons := input.EncodeMouseButton(m.buttons)
|
|
|
|
m.page.browser.trySlowMotion()
|
|
|
|
err := proto.InputDispatchMouseEvent{
|
|
Type: proto.InputDispatchMouseEventTypeMouseMoved,
|
|
X: p.X,
|
|
Y: p.Y,
|
|
Button: button,
|
|
Buttons: gson.Int(buttons),
|
|
Modifiers: m.page.Keyboard.getModifiers(),
|
|
}.Call(m.page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// to make sure set only when call is successful
|
|
m.pos = p
|
|
|
|
if m.page.browser.trace {
|
|
if !m.updateMouseTracer() {
|
|
m.initMouseTracer()
|
|
m.updateMouseTracer()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MoveAlong the guide function.
|
|
// Every time the guide function is called it should return the next mouse position, return true to stop.
|
|
// Read the source code of [Mouse.MoveLinear] as an example to use this method.
|
|
func (m *Mouse) MoveAlong(guide func() (proto.Point, bool)) error {
|
|
for {
|
|
p, stop := guide()
|
|
if stop {
|
|
return m.MoveTo(p)
|
|
}
|
|
|
|
err := m.MoveTo(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// MoveLinear to the absolute position with the given steps.
|
|
// Such as move from (0,0) to (6,6) with 3 steps, the mouse will first move to (2,2) then (4,4) then (6,6).
|
|
func (m *Mouse) MoveLinear(to proto.Point, steps int) error {
|
|
p := m.Position()
|
|
step := to.Minus(p).Scale(1 / float64(steps))
|
|
count := 0
|
|
|
|
return m.MoveAlong(func() (proto.Point, bool) {
|
|
count++
|
|
if count == steps {
|
|
return to, true
|
|
}
|
|
|
|
p = p.Add(step)
|
|
return p, false
|
|
})
|
|
}
|
|
|
|
// Scroll the relative offset with specified steps.
|
|
func (m *Mouse) Scroll(offsetX, offsetY float64, steps int) error {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
defer m.page.tryTrace(TraceTypeInput, fmt.Sprintf("scroll (%.2f, %.2f)", offsetX, offsetY))()
|
|
m.page.browser.trySlowMotion()
|
|
|
|
if steps < 1 {
|
|
steps = 1
|
|
}
|
|
|
|
button, buttons := input.EncodeMouseButton(m.buttons)
|
|
|
|
stepX := offsetX / float64(steps)
|
|
stepY := offsetY / float64(steps)
|
|
|
|
for i := 0; i < steps; i++ {
|
|
err := proto.InputDispatchMouseEvent{
|
|
Type: proto.InputDispatchMouseEventTypeMouseWheel,
|
|
Button: button,
|
|
Buttons: gson.Int(buttons),
|
|
Modifiers: m.page.Keyboard.getModifiers(),
|
|
DeltaX: stepX,
|
|
DeltaY: stepY,
|
|
X: m.pos.X,
|
|
Y: m.pos.Y,
|
|
}.Call(m.page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Down holds the button down.
|
|
func (m *Mouse) Down(button proto.InputMouseButton, clickCount int) error {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
toButtons := append(append([]proto.InputMouseButton{}, m.buttons...), button)
|
|
|
|
_, buttons := input.EncodeMouseButton(toButtons)
|
|
|
|
err := proto.InputDispatchMouseEvent{
|
|
Type: proto.InputDispatchMouseEventTypeMousePressed,
|
|
Button: button,
|
|
Buttons: gson.Int(buttons),
|
|
ClickCount: clickCount,
|
|
Modifiers: m.page.Keyboard.getModifiers(),
|
|
X: m.pos.X,
|
|
Y: m.pos.Y,
|
|
}.Call(m.page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.buttons = toButtons
|
|
return nil
|
|
}
|
|
|
|
// Up releases the button.
|
|
func (m *Mouse) Up(button proto.InputMouseButton, clickCount int) error {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
|
|
toButtons := []proto.InputMouseButton{}
|
|
for _, btn := range m.buttons {
|
|
if btn == button {
|
|
continue
|
|
}
|
|
toButtons = append(toButtons, btn)
|
|
}
|
|
|
|
_, buttons := input.EncodeMouseButton(toButtons)
|
|
|
|
err := proto.InputDispatchMouseEvent{
|
|
Type: proto.InputDispatchMouseEventTypeMouseReleased,
|
|
Button: button,
|
|
Buttons: gson.Int(buttons),
|
|
ClickCount: clickCount,
|
|
Modifiers: m.page.Keyboard.getModifiers(),
|
|
X: m.pos.X,
|
|
Y: m.pos.Y,
|
|
}.Call(m.page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.buttons = toButtons
|
|
return nil
|
|
}
|
|
|
|
// Click the button. It's the combination of [Mouse.Down] and [Mouse.Up].
|
|
func (m *Mouse) Click(button proto.InputMouseButton, clickCount int) error {
|
|
m.page.browser.trySlowMotion()
|
|
|
|
err := m.Down(button, clickCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return m.Up(button, clickCount)
|
|
}
|
|
|
|
// Touch presents a touch device, such as a hand with fingers, each finger is a [proto.InputTouchPoint].
|
|
// Touch events is stateless, we use the struct here only as a namespace to make the API style unified.
|
|
type Touch struct {
|
|
page *Page
|
|
}
|
|
|
|
func (p *Page) newTouch() *Page {
|
|
p.Touch = &Touch{page: p}
|
|
return p
|
|
}
|
|
|
|
// Start a touch action.
|
|
func (t *Touch) Start(points ...*proto.InputTouchPoint) error {
|
|
// TODO: https://crbug.com/613219
|
|
_ = t.page.WaitRepaint()
|
|
_ = t.page.WaitRepaint()
|
|
|
|
return proto.InputDispatchTouchEvent{
|
|
Type: proto.InputDispatchTouchEventTypeTouchStart,
|
|
TouchPoints: points,
|
|
Modifiers: t.page.Keyboard.getModifiers(),
|
|
}.Call(t.page)
|
|
}
|
|
|
|
// Move touch points. Use the [proto.InputTouchPoint.ID] (Touch.identifier) to track points.
|
|
// Doc: https://developer.mozilla.org/en-US/docs/Web/API/Touch_events
|
|
func (t *Touch) Move(points ...*proto.InputTouchPoint) error {
|
|
return proto.InputDispatchTouchEvent{
|
|
Type: proto.InputDispatchTouchEventTypeTouchMove,
|
|
TouchPoints: points,
|
|
Modifiers: t.page.Keyboard.getModifiers(),
|
|
}.Call(t.page)
|
|
}
|
|
|
|
// End touch action.
|
|
func (t *Touch) End() error {
|
|
return proto.InputDispatchTouchEvent{
|
|
Type: proto.InputDispatchTouchEventTypeTouchEnd,
|
|
TouchPoints: []*proto.InputTouchPoint{},
|
|
Modifiers: t.page.Keyboard.getModifiers(),
|
|
}.Call(t.page)
|
|
}
|
|
|
|
// Cancel touch action.
|
|
func (t *Touch) Cancel() error {
|
|
return proto.InputDispatchTouchEvent{
|
|
Type: proto.InputDispatchTouchEventTypeTouchCancel,
|
|
TouchPoints: []*proto.InputTouchPoint{},
|
|
Modifiers: t.page.Keyboard.getModifiers(),
|
|
}.Call(t.page)
|
|
}
|
|
|
|
// Tap dispatches a touchstart and touchend event.
|
|
func (t *Touch) Tap(x, y float64) error {
|
|
defer t.page.tryTrace(TraceTypeInput, "touch")()
|
|
t.page.browser.trySlowMotion()
|
|
|
|
p := &proto.InputTouchPoint{X: x, Y: y}
|
|
|
|
err := t.Start(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return t.End()
|
|
}
|