/lune-sonar

A Luau library built for Lune which attempts to find users in a given Roblox place.

Primary LanguageLuauMIT LicenseMIT

Logo

Sonar

A Luau library built for Lune which attempts to find users in a given Roblox place

Disclaimer: This is purely a proof of concept, it was made as an experiment with Roblox's thumbnail URL caching. This wasn't made with the goal of breaking Roblox TOS.

How it works

The Thumbnails API has a /v1/batch endpoint which "returns a list of thumbnails with varying types and sizes".

Let's make a quick request.

[
  {
    "requestId": "foo",
    "targetId": 1,
    "type": "AvatarHeadshot",
    "size": "150x150",
    "format": "webp"
  }
]

This should give us back a headshot of @Roblox's avatar, because targetId in this case is set to @Roblox's user ID.

And it does!

{
  "data": [
    {
      "requestId": "foo",
      "targetId": 1,
      "imageUrl": "https://tr.rbxcdn.com/30DAY-AvatarHeadshot-310966282D3529E36976BF6B07B1DC90-Png/150/150/AvatarHeadshot/Webp/noFilter",
      ...
    }
  ]
}

Now we've got a valid image URL for @Roblox's headshot. But this is not the only thing you can do with this.

If you take a quick look at imageUrl, you can notice a hexadecimal string in the middle: 310966282D3529E36976BF6B07B1DC90. We'll call these "thumbnail tokens".

Another place where user thumbnails are used is in the server list for Roblox places. You can work with these using the Games API, specifically the /v1/games/{placeId}/servers/{serverType} endpoint.

This endpoint allows us to iterate through the servers of any place with its ID. Let's take a look at one of the entries in a response.

{
  "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "playerTokens": [
    "199FBB623957CE5E4F7838A203402349",
    "F83F5E9A514950E291F0C2B12BF6EA22",
    "E2DA836A8B63706CAA462BAB6ADFB6FF",
    "583E65B5DF74D76E5A6AF841623368E3",
    "208FAFE28DC51537BA79540D697762F8"
  ],
  ...
}

It contains a playerTokens field. You can pass one of those to the Thumbnails API using the token field in a request. Let's put one of those playerTokens in there.

[
  {
    "requestId": "baz",
    "token": "199FBB623957CE5E4F7838A203402349",
    "type": "AvatarHeadshot",
    "size": "150x150",
    "format": "webp"
  }
]

This should give us a response similar to the last one, which means we can extract a thumbnail token from the response here as well.

The trick with these thumbnail tokens is that they're cached based on the user, size, format, etc... for 30 days, this means that as long as we pass the same parameters (size, format, etc...) to both thumbnail requests, we will get the same imageUrl for a specific user, regardless of whether we used a user ID or a playerToken.

lune-sonar abstracts these thumbnail tokens behind an interface called a "fingerprint". You can associate user IDs to these fingerprints.

The library's lookup function takes a list of user IDs along with a place ID. It starts by collecting thumbnails from the user IDs and turning them into fingerprints. It proceeds by iterating through the place's servers all the while collecting thumbnails from playerTokens and turning them into fingerprints aswell.

It can then compare fingerprints made from the playerTokens with the fingerprints made from the list of user IDs, and keep track of the results.

API

PartialSearchOptions

type PartialSearchOptions = {
    -- default: public
    serverType: ("public" | "private")?,
}

Options to configure the searching process, used by searchForUsers.

SearchResult

type SearchResult = {
    serverId: string,
    serverPing: number,
    serverFps: number,
    activePlayers: number,
    maxPlayers: number,
    userIds: { number },
}

Search result containing server information, returned by searchForUsers.

searchForUsers

function searchForUsers(
    userIds: { number },
    placeId: number,
    partialSearchOptions: PartialSearchOptions?
) -> { SearchResult }

Searches for users in a given place and returns the results.