Using xdotool and Websockets to Automate the OnePlus Clicker Game

Using xdotool and Websockets to Automate the OnePlus Clicker Game

Last month OnePlus unveiled a promotion for their then-upcoming OnePlus 6T. The premise was simple: tap or click the screen to unlock mystery prizes. The more you clicked, the better the prize would be. After clicking to the first prize, just 60 clicks, I decided try automating the process.

If you go the game’s website from your desktop, you’ll get a message asking you to open it on mobile. Luckily, it’s easy enough to simulate a mobile device using the Google Chrome developer tools.

xdotool

Clicking a few thousand times using a tool like AutoHotkey would be easy. Unfortunately AutoHotKey is not available on linux, but there is an alternative: xdotool. With a few quick Google searches I was able to setup this bash script to start clicking for me. Within a few hours I was up to 60,000 clicks.

The next milestone was 300,000 clicks. I wanted to give it a shot to see what prizes laid behind such a massive number of clicks. Unfortunately, there were several downsides to the xdotool method:

  • I could no longer use my computer

  • It was relatively slow

  • The browser would sometimes crash (likely due to the devtools storing a lot of info)

Websockets

When checking the network requests I found that the game wasn’t using regular HTTP requests but rather sending all the info through websockets. I had experience simulating HTTP requests but wasn’t sure how to go about simulating websocket frames.

First, I tried simulating a click on the screen using plain old javascript. After taking the code from this stackoverflow question, I was able to get the click event to dispatch but for some reason it wasn’t increasing the counter. Puzzled, I turned on the JS debugger just before firing the click event. Somewhere in the jumble of minified JS the script was checking if the click event was legit using the isTrusted property. Essentially, isTrusted is a security feature in browsers to see if the click was generated by the user or javascript. I didn’t find any simple way around it.

When digging through the JavaScript I got the idea to bypass the event handler and execute the code directly that was sending the message to the websocket. Between the minified code and my lack of familiarity with Websockets I couldn’t manage find the exact code that was sending the frame.

Example frames in Google Chrome

Next, I tried to find the Websocket object in memory to send frames myself using that. I had no luck finding the object in memory. What if I just make a new connection?

From what I could see, there were really only two kinds of frames being sent: one for authentication and a frame for each click event.

Authentication frame*:

420[
  "authenticate user",
  {
    "username": "raybb",
    "token": "ff45fb994e3124a5e0ce7bf7f27745dcbe62e71275532f982498b798",
    "squad": "",
    "locale": "en-gb",
    "count": 62301,
    "csrf": "0xsn5yq9SAb65OvTXDl3OjzRoVPgr1zVvDtzoVR4ZhjdptbHClRSVaSJDXKwXy8b"
  }
]

Examples of the frames (1st, 2nd, and 10th)*:

421[
    "tap_logs",
    {
        "username": "raybb", "timestamp": 1540163213460,
        "touchX": 19, "touchY": 78
    }
]
422["tap_logs", {"username": "raybb", "timestamp": 1540163297196, "touchX": 389, "touchY": 737}]
4200["tap_logs", { "username": "raybb", "timestamp": 1540163347125, "touchX": 306, "touchY": 442}]

*these frames have modified values for privacy reasons.

From experimenting, I found:

  • token and csrf: didn’t change between refreshes

  • count: the number of taps I had when page loaded (changing it had no impact)

  • The auth frame always started with 420 (which we shall call the frame counter)

  • The frame counter mostly increased by 1

  • The leftmost digits of the frame counter never reached 43. For example: [420, 421, 422, ..., 429, 4200, 4201, ..., 4299, 42000, ...]

With this in mind, I made my first attempt to simulate a click with a websocket:

const socket = new WebSocket('wss://socket-unlock.oneplus.com/socket.io/?EIO=3&transport=websocket');

socket.addEventListener('open', function(event {
    console.log("opened");
    socket.send(authFrame);
    socket.send(firstTap);
});

It would connect successfully but the first tap wouldn’t send 🤔

It turns out that the server only accepts taps after it has sent a response confirming authentication. The confirmation frame always starts with 430 and contains your username. So all I had to do was wait for that frame to come back and then send my taps.

const socket = new WebSocket('wss://socket-unlock.oneplus.com/socket.io/?EIO=3&transport=websocket');

socket.addEventListener('open', function(event) {
    console.log("opened");
    socket.send(authFrame);
});

socket.addEventListener('message', function(event) {
    if (event.data.indexOf("raybb") > -1) {
        if (event.data.indexOf("430") > -1) {
        socket.send(firstTap);
        }
    }
});

After figuring that out, it was pretty easy to write the rest of the code. You can see the full script here.

The only issue with this method was that sending taps would eventually slow down. After sending a few thousand taps the script would get noticeably slower, and stay at that speed. I suspect it was due to the recursive nature of the script, which makes many layers of scoped variables to build. I changed the code to send 100 taps in each level of recursion. That seemed to help a bit but the slowing was still noticeable. Then I blocked all scripts from loading on the OnePlus website (so it was essentially just an empty page). That also seemed to help a bit.

I didn’t end up finding a solution to the slowing script. The contest was ending before too long. It wasn’t much of an issue and before I knew it I was on my way to 1 million taps. I may not have won any prizes from this but I did learn about click events, Websockets, xdotool, and a little more about poking through sites with the debugger.

Thanks for reading 🤓👏

PS: This blog post was originally published on Medium, back before all the paywalls. It was moved here April 2024.