roblox-deeplink-parser 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/.github/FUNDING.yml +3 -0
- package/.github/workflows/publish.yaml +29 -0
- package/.vscode/settings.json +8 -0
- package/LICENSE +21 -0
- package/README.md +78 -0
- package/biome.json +28 -0
- package/jsr.json +8 -0
- package/package.json +27 -0
- package/src/AuthedProtocolParser.ts +292 -0
- package/src/DeepLinkParser.ts +473 -0
- package/src/ParsedDeepLink.ts +114 -0
- package/src/utils/constants.ts +14 -0
- package/src/utils/deepLinks.ts +1251 -0
- package/tsconfig.json +30 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish on JSR/
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
release:
|
|
7
|
+
types: [created]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
name: Build
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Setup Repo
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Bun
|
|
23
|
+
uses: oven-sh/setup-bun@v2
|
|
24
|
+
with:
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
26
|
+
|
|
27
|
+
- name: Publish on JSR
|
|
28
|
+
if: github.event_name == 'release' && github.repository == 'RoSeal-Extension/Roblox-DeepLink-Parser'
|
|
29
|
+
run: bunx jsr publish --allow-slow-types --allow-dirty
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Julli4n
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Roblox DeepLink Parser
|
|
2
|
+
Parse Roblox deeplinks (roblox://*), website links (www.roblox.com), and appsflyer links (ro.blox.com) to get params and get new appysflyer/deeplink/website URLs.
|
|
3
|
+
|
|
4
|
+
## Example
|
|
5
|
+
```ts
|
|
6
|
+
import DeepLinkParser from "@roseal/roblox-deeplink-parser";
|
|
7
|
+
const parser = new DeepLinkParser();
|
|
8
|
+
|
|
9
|
+
// same as parser.parseWebsiteLink
|
|
10
|
+
parser.parseLink("https://www.roblox.com/my/avatar").then((link) => {
|
|
11
|
+
if (link) {
|
|
12
|
+
// roblox://navigation/avatar
|
|
13
|
+
console.log(link.toProtocolUrl());
|
|
14
|
+
// https://www.roblox.com/my/avatar
|
|
15
|
+
console.log(link.toWebsiteUrl());
|
|
16
|
+
// https://ro.blox.com/Ebh5?af_dp=roblox%3A%2F%2Fnavigation%2Favatar&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fmy%2Favatar
|
|
17
|
+
console.log(link.toAppsFlyerUrl());
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ... and you can do these as well specifically:
|
|
22
|
+
parser.parseProtocolLink("roblox://navigation/avatar")
|
|
23
|
+
parser.parseAppsFlyerLink("https://ro.blox.com/Ebh5?af_dp=roblox%3A%2F%2Fnavigation%2Favatar&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fmy%2Favatar");
|
|
24
|
+
parser.parseWebsiteLink("https://www.roblox.com/my/avatar");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Reference
|
|
28
|
+
| Deeplink | Website Equivalent | Description |
|
|
29
|
+
| -------- | ------------------ | ----------- |
|
|
30
|
+
| `roblox://experiences/start` or `roblox://` <br />Search Parameters:<br />- placeId<br />- userId<br />- gameInstanceId<br /> - accessCode<br />- linkCode<br />- launchData<br /> - joinAttemptId<br />- joinAttemptOrigin<br />- reservedServerAccessCode<br />- callId<br />- browserTrackerId<br />- referralPage<br />- referredByPlayerId<br />- eventId<br />- isoContext | `www.roblox.com/games/start` <br />Search Parameters:<br />- placeId<br /> - gameInstanceId<br /> - accessCode<br />- linkCode<br />- launchData<br /> - joinAttemptId<br />- joinAttemptOrigin<br />- reservedServerAccessCode<br />- callId<br />- browserTrackerId<br />- referralPage<br />- referredByPlayerId | Join a server or user, for more information please see [Bloxstrap wiki](https://github.com/bloxstraplabs/bloxstrap/wiki/A-deep-dive-on-how-the-Roblox-bootstrapper-works#starting-roblox)
|
|
31
|
+
| `roblox://navigation/profile` <br />Search Parameters:<br />- groupId<br />- userId<br />- friendshipSourceType | `www.roblox.com/users/{userId}/profile` or `www.roblox.com/communities/{groupId}/name` | Open webview of a user or community on the website
|
|
32
|
+
| `roblox://navigation/group` <br />Search Parameters:<br />- groupId<br />- forumCategoryId<br />- forumPostId<br />- forumCommentId| `www.roblox.com/communities/{groupId}/name` | Open webview of a community page
|
|
33
|
+
| `roblox://navigation/profile_card` <br />Search Parameters:<br />- userId | `www.roblox.com/users/{userId}/profile` | Opens the native app view of a user profile
|
|
34
|
+
| `roblox://navigation/content_posts` <br />Search Parameters:<br />- userId<br />- postId | N/A | Show content posts "captures" from a user or a specific post
|
|
35
|
+
| `roblox://navigation/share_links` <br />Search Parameters:<br />- type<br />- code | `www.roblox.com/share-links` <br />Search Parameters:<br />- type<br />- code | Resolve a share link, which could be a friend invite, experience details link, experience invite, etc.
|
|
36
|
+
| `roblox://navigation/gift_cards` | `www.roblox.com/giftcards` | Open webview of the giftcards page
|
|
37
|
+
| `roblox://navigation/external_web_link` <br />Search Parameters:<br />- domain (zendesk)<br />- locale<br />- articleId | `en.help.roblox.com/hc/{locale}/articles/{articleId}` | Open webview of a zendesk article
|
|
38
|
+
| `roblox://navigation/chat` <br />Search Parameters:<br />- userId<br />- chatId<br />- entryPoint | N/A | Open chat conversation with a user or another type of conversation (group)
|
|
39
|
+
| `roblox://navigation/appeals` <br />Search Parameters:<br />- vid | `www.roblox.com/report-appeals#/v/{vid}` | Open webview of appeals page or a certain appeal request
|
|
40
|
+
| `roblox://navigation/home` | `www.roblox.com/home` | Open home page
|
|
41
|
+
| `roblox://navigation/event_details` <br />Search Parameters:<br />- eventId | `www.roblox.com/events/{eventId}` | Open details of an experience event
|
|
42
|
+
| `roblox://navigation/crossdevice` <br />Search Parameters:<br />- code | `www.roblox.com/crossdevicelogin/confirmcode` | Open crossdevice login page, and automatically enter the code if there was one in the deeplink
|
|
43
|
+
| `roblox://navigation/contacts` <br />Search Parameters:<br />- contactId<br />- assetId<br />- avatarImageUrl | N/A | Open contacts page (or friend requests page)
|
|
44
|
+
| `roblox://navigation/avatar_clothing_sort` | N/A | Open avatar clothing sorting page
|
|
45
|
+
| `roblox://navigation/avatar_profile_picture_editor` | N/A | Open avatar profile picture editor page
|
|
46
|
+
| `roblox://navigation/catalog` | `www.roblox.com/catalog` | Open avatar marketplace page
|
|
47
|
+
| `roblox://navigation/catalog/equip` <br />Search Parameters:<br />- itemId<br />- itemType (Asset\|Bundle) | N/A | Equip or try on an avatar item
|
|
48
|
+
| `roblox://navigation/friends` | `www.roblox.com/users/friends` | View the currently authenticated user's friends
|
|
49
|
+
| `roblox://navigation/avatar` <br />Search Parameters:<br />- itemType (Character)<br />- itemId | `www.roblox.com/my/avatar` | Open the avatar editor page
|
|
50
|
+
| `roblox://navigation/more` | N/A | Open the More section of the app
|
|
51
|
+
| `roblox://navigation/games` | `www.roblox.com/charts` | Open the charts page
|
|
52
|
+
| `roblox://navigation/sort` <br />Search Parameters:<br />- sortName | `www.roblox.com/charts#/sortName/{sortId}` | Open the charts sort page
|
|
53
|
+
| `roblox://navigation/item_details` <br />Search Parameters:<br />- itemType (Look\|Asset\|Bundle)<br />- itemId | `www.roblox.com/catalog/{itemId}/name`, `www.roblox.com/bundles/{itemId}/name`, or `www.roblox.com/looks/{itemId}/name` | Open the details page of an avatar asset, bundle, or look
|
|
54
|
+
| `roblox://navigation/account_info` | `www.roblox.com/my/account#!/info` | Open webview of the account settings info page
|
|
55
|
+
| `roblox://navigation/notification_settings` | `www.roblox.com/my/account#!/notifications` | Open webview of the account settings notifications page
|
|
56
|
+
| `roblox://navigation/privacy_settings` | `www.roblox.com/my/account#!/privacy` | Open webview of the account settings privacy page
|
|
57
|
+
| `roblox://navigation/parental_controls` | `www.roblox.com/my/account#!/parental-controls` | Open webview of the account settings parental controls page
|
|
58
|
+
| `roblox://navigation/spending_settings` | `www.roblox.com/my/account#!/payment-methods` | Open webview of the account settings payment methods page
|
|
59
|
+
| `roblox://navigation/qr_code_redemption` <br />Search Parameters:<br />- itemType (Asset\|Bundle)<br />- itemId | N/A | Redeem an item which can be obtained by scanning a QR code
|
|
60
|
+
| `roblox://navigation/game_details` <br />Search Parameters:<br />- gameId<br />- privateServerLinkCode | `www.roblox.com/games/{placeId}/name` <br />Search Parameters:<br />- privateServerLinkCode | Opens the details page of an experience
|
|
61
|
+
| `roblox://navigation/security_alert` <br />Search Parameters:<br />- payload | `www.roblox.com/security-feedback` <br />Search Parameters:<br />- payload | Opens security feedback page
|
|
62
|
+
| `roblox://navigation/experience_sort` <br />Search Parameters:<br />- sortId | N/A | Opens an experience sort page on the home page
|
|
63
|
+
| `roblox://navigation/party` | N/A | Opens the party page
|
|
64
|
+
| `roblox://navigation/app_permissions_settings` | `www.roblox.com/my/account#!/app-permissions` | Opens the app permissions page
|
|
65
|
+
| `roblox://navigation/screentime_subsettings` | `www.roblox.com/my/account#!/privacy/Screentime` | Opens the screentime subsettings page
|
|
66
|
+
| `roblox://navigation/blocked_experiences_subsettings` | `www.roblox.com/my/account#!/privacy/ContentRestrictions/BlockedExperiences` | Opens the blocked experiences subsettings page
|
|
67
|
+
| `roblox://navigation/blocked_users_subsettings` | `www.roblox.com/my/account#!/privacy/BlockedUsers` | Opens the blocked users subsettings page
|
|
68
|
+
| `roblox://navigation/experience_chat_subsettings` | `www.roblox.com/my/account#!/privacy/ExperienceChat` | Opens the experience chat subsettings page
|
|
69
|
+
| `roblox://navigation/party_subsettings` | `www.roblox.com/my/account#!/privacy/Party` | Opens the party subsettings page
|
|
70
|
+
| `roblox://navigation/voice_subsettings` | `www.roblox.com/my/account#!/privacy/Voice` | Opens the voice subsettings page
|
|
71
|
+
| `roblox://navigation/trading_inventory_subsettings` | `www.roblox.com/my/account#!/privacy/TradingInventory` | Opens the trading inventory subsettings page
|
|
72
|
+
| `roblox://navigation/friends_contacts_subsettings` | `www.roblox.com/my/account#!/privacy/FriendsAndContacts` | Opens the friends and contacts subsettings page
|
|
73
|
+
| `roblox://navigation/private_server_subsettings` | `www.roblox.com/my/account#!/privacy/PrivateServers` | Opens the private server subsettings page
|
|
74
|
+
| `roblox://navigation/visibility_subsettings` | `www.roblox.com/my/account#!/privacy/Visibility` | Opens the visibility subsettings page
|
|
75
|
+
| `roblox://navigation/fae` | N/A | Opens the facial age estimation page
|
|
76
|
+
| `roblox://navigation/fae_upsell_overlay` | N/A | Opens the facial age estimation overlay page
|
|
77
|
+
| `roblox://navigation/account_recovery` <br />Search Parameters:<br />- origin<br />- username<br />- recoverySessionId | `www.roblox.com/login/forgot-password-or-username` <br />Search Parameters:<br />- origin<br />- username<br />- recoverySessionId | Opens the account recovery page
|
|
78
|
+
| `roblox://navigation/messages` | `https://www.roblox.com/my/messages` | Opens the systen messages page
|
package/biome.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["**"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "tab"
|
|
15
|
+
},
|
|
16
|
+
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
|
17
|
+
"linter": {
|
|
18
|
+
"enabled": true,
|
|
19
|
+
"rules": {
|
|
20
|
+
"recommended": true
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"javascript": {
|
|
24
|
+
"formatter": {
|
|
25
|
+
"quoteStyle": "double"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
package/jsr.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roblox-deeplink-parser",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.20",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/DeepLinkParser.js",
|
|
7
|
+
"module": "./dist/DeepLinkParser.js",
|
|
8
|
+
"types": "./dist/DeepLinkParser.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/DeepLinkParser.js",
|
|
12
|
+
"types": "./dist/DeepLinkParser.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./authedProtocol": {
|
|
15
|
+
"import": "./dist/AuthedProtocolParser.js",
|
|
16
|
+
"types": "./dist/AuthedProtocolParser.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/bun": "latest",
|
|
21
|
+
"typescript": "latest",
|
|
22
|
+
"@biomejs/biome": "latest"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bunx bunup src/DeepLinkParser.ts src/AuthedProtocolParser.ts"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PLACELAUNCHER_URL,
|
|
3
|
+
DEFAULT_ROBLOX_PLAYER_AUTHED_PROTOCOL,
|
|
4
|
+
DEFAULT_ROBLOX_STUDIO_AUTH_AUTHED_PROTOCOL,
|
|
5
|
+
DEFAULT_ROBLOX_STUDIO_AUTHED_PROTOCOL,
|
|
6
|
+
} from "./utils/constants";
|
|
7
|
+
|
|
8
|
+
export type AuthedProtocolUrls<T> = {
|
|
9
|
+
robloxPlayerProtocol: T;
|
|
10
|
+
robloxStudioProtocol: T;
|
|
11
|
+
robloxStudioAuthProtocol: T;
|
|
12
|
+
placeLauncherUrl: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AuthedProtocolParserConstructorProps<T extends string> = {
|
|
16
|
+
urls?: Partial<AuthedProtocolUrls<T>>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type AuthedLaunchMode =
|
|
20
|
+
| "edit"
|
|
21
|
+
| "plugin"
|
|
22
|
+
| "play"
|
|
23
|
+
| "build"
|
|
24
|
+
| "app"
|
|
25
|
+
| "asset";
|
|
26
|
+
|
|
27
|
+
export type AuthLaunchExp = "InApp" | "PreferInApp" | "InBrowser";
|
|
28
|
+
|
|
29
|
+
export type AuthedStudioTask =
|
|
30
|
+
| "EditFile"
|
|
31
|
+
| "EditPlace"
|
|
32
|
+
| "EditPlaceRevision"
|
|
33
|
+
| "StartClient"
|
|
34
|
+
| "StartTeamTest"
|
|
35
|
+
| "InstallPlugin"
|
|
36
|
+
| "TryAsset"
|
|
37
|
+
| "RemoteDebug"
|
|
38
|
+
| "StartServer";
|
|
39
|
+
|
|
40
|
+
export type AuthedDistributorType = "Global" | "ChinaJoinVenture";
|
|
41
|
+
|
|
42
|
+
export type AuthedOtherParams = {
|
|
43
|
+
browsertrackerid?: number;
|
|
44
|
+
robloxLocale?: string;
|
|
45
|
+
gameLocale?: string;
|
|
46
|
+
channel?: string;
|
|
47
|
+
LaunchExp?: AuthLaunchExp;
|
|
48
|
+
avatar?: string;
|
|
49
|
+
assetid?: number;
|
|
50
|
+
pluginid?: number;
|
|
51
|
+
placeid?: number;
|
|
52
|
+
universeid?: number;
|
|
53
|
+
script?: string;
|
|
54
|
+
placelauncherurl?: string;
|
|
55
|
+
task?: AuthedStudioTask;
|
|
56
|
+
traceId?: string;
|
|
57
|
+
userId?: number;
|
|
58
|
+
browser?: string;
|
|
59
|
+
distributorType?: AuthedDistributorType;
|
|
60
|
+
[key: string]: string | number | undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type BuildAuthedProtocolUrlParameters<T extends string> = {
|
|
64
|
+
type: T;
|
|
65
|
+
launchMode?: AuthedLaunchMode;
|
|
66
|
+
gameInfo?: string;
|
|
67
|
+
launchTime?: string;
|
|
68
|
+
baseUrl?: string;
|
|
69
|
+
otherParams?: AuthedOtherParams;
|
|
70
|
+
|
|
71
|
+
// auth params
|
|
72
|
+
state?: string;
|
|
73
|
+
code?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type BuildAuthedPlaceLauncherRequest =
|
|
77
|
+
| "RequestGame"
|
|
78
|
+
| "RequestCloudEdit"
|
|
79
|
+
| "RequestGameJob"
|
|
80
|
+
| "RequestFollowUser"
|
|
81
|
+
| "RequestPrivateGame"
|
|
82
|
+
| "RequestPlayTogetherGame"
|
|
83
|
+
| "RequestGameApp"
|
|
84
|
+
| "RequestInvalid"
|
|
85
|
+
| "RequestPlayWithParty"
|
|
86
|
+
| "CheckGameJobStatus"
|
|
87
|
+
| "RequestReservedGame"
|
|
88
|
+
| "RequestCrossExpVoice";
|
|
89
|
+
|
|
90
|
+
export type AuthedPrivateGameMode = "ReservedServer";
|
|
91
|
+
|
|
92
|
+
export type BuildAuthedPlaceLauncherURLParameters<T extends string> = {
|
|
93
|
+
request: BuildAuthedPlaceLauncherRequest;
|
|
94
|
+
placeId?: number;
|
|
95
|
+
jobId?: string;
|
|
96
|
+
gameId?: string;
|
|
97
|
+
gender?: string;
|
|
98
|
+
genderId?: number;
|
|
99
|
+
accessCode?: string;
|
|
100
|
+
linkCode?: string;
|
|
101
|
+
launchData?: string;
|
|
102
|
+
privateGameMode?: AuthedPrivateGameMode;
|
|
103
|
+
teleportType?: string;
|
|
104
|
+
reservedServerAccessCode?: string;
|
|
105
|
+
referralPage?: string;
|
|
106
|
+
referredByPlayerId?: number;
|
|
107
|
+
conversationId?: number;
|
|
108
|
+
isPartyLeader?: boolean;
|
|
109
|
+
isTeleport?: boolean;
|
|
110
|
+
partyGuid?: string;
|
|
111
|
+
isPlayTogetherGame?: boolean;
|
|
112
|
+
joinAttemptId?: string;
|
|
113
|
+
joinAttemptOrigin?: T;
|
|
114
|
+
callId?: string;
|
|
115
|
+
browserTrackerId?: number;
|
|
116
|
+
eventId?: string;
|
|
117
|
+
isolationContext?: string;
|
|
118
|
+
gameJoinContext?: string;
|
|
119
|
+
userId?: number;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default class AuthedProtocolParser<T extends string, U extends string> {
|
|
123
|
+
public _urls: AuthedProtocolUrls<T> = {
|
|
124
|
+
robloxPlayerProtocol: DEFAULT_ROBLOX_PLAYER_AUTHED_PROTOCOL as T,
|
|
125
|
+
robloxStudioProtocol: DEFAULT_ROBLOX_STUDIO_AUTHED_PROTOCOL as T,
|
|
126
|
+
robloxStudioAuthProtocol: DEFAULT_ROBLOX_STUDIO_AUTH_AUTHED_PROTOCOL as T,
|
|
127
|
+
placeLauncherUrl: DEFAULT_PLACELAUNCHER_URL,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
constructor(props?: AuthedProtocolParserConstructorProps<T>) {
|
|
131
|
+
if (props?.urls) {
|
|
132
|
+
if (props.urls.robloxPlayerProtocol) {
|
|
133
|
+
this._urls.robloxPlayerProtocol = props.urls.robloxPlayerProtocol;
|
|
134
|
+
}
|
|
135
|
+
if (props.urls.robloxStudioProtocol) {
|
|
136
|
+
this._urls.robloxStudioProtocol = props.urls.robloxStudioProtocol;
|
|
137
|
+
}
|
|
138
|
+
if (props.urls.robloxStudioAuthProtocol) {
|
|
139
|
+
this._urls.robloxStudioAuthProtocol =
|
|
140
|
+
props.urls.robloxStudioAuthProtocol;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (props.urls.placeLauncherUrl) {
|
|
144
|
+
this._urls.placeLauncherUrl = props.urls.placeLauncherUrl;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public buildAuthedProtocolUrl(
|
|
150
|
+
request: BuildAuthedProtocolUrlParameters<T>,
|
|
151
|
+
): string {
|
|
152
|
+
if (
|
|
153
|
+
request.type === this._urls.robloxStudioAuthProtocol &&
|
|
154
|
+
request.state &&
|
|
155
|
+
request.code
|
|
156
|
+
) {
|
|
157
|
+
return `${this._urls.robloxStudioAuthProtocol}://?state=${request.state}&code=${request.code}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const params: string[] = ["1"];
|
|
161
|
+
|
|
162
|
+
if (request.launchMode) params.push(`launchmode:${request.launchMode}`);
|
|
163
|
+
if (request.gameInfo) params.push(`gameinfo:${request.gameInfo}`);
|
|
164
|
+
if (request.launchTime) params.push(`launchtime:${request.launchTime}`);
|
|
165
|
+
if (request.baseUrl) params.push(`baseUrl:${request.baseUrl}`);
|
|
166
|
+
|
|
167
|
+
if (request.otherParams) {
|
|
168
|
+
for (const key in request.otherParams) {
|
|
169
|
+
const value = request.otherParams[key];
|
|
170
|
+
if (key === value) {
|
|
171
|
+
params.push(key);
|
|
172
|
+
} else if (value !== undefined && value !== null) {
|
|
173
|
+
params.push(`${key}:${encodeURIComponent(value)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return `${request.type}:${params.join("+")}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public buildAuthedPlaceLauncherUrl(
|
|
182
|
+
request: BuildAuthedPlaceLauncherURLParameters<U>,
|
|
183
|
+
): string {
|
|
184
|
+
const url = new URL(this._urls.placeLauncherUrl);
|
|
185
|
+
const stringifiedParameters: Record<string, string> = {};
|
|
186
|
+
for (const key in request) {
|
|
187
|
+
const value = request[key as keyof typeof request];
|
|
188
|
+
|
|
189
|
+
if (value !== undefined && value !== null) {
|
|
190
|
+
stringifiedParameters[key] = String(value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
url.search = new URLSearchParams(stringifiedParameters).toString();
|
|
195
|
+
return url.toString();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public parseAuthedProtocolUrl(
|
|
199
|
+
url: string,
|
|
200
|
+
): BuildAuthedProtocolUrlParameters<T> | null {
|
|
201
|
+
if (url.startsWith(`${this._urls.robloxStudioAuthProtocol}:`)) {
|
|
202
|
+
const urlObj = new URL(url);
|
|
203
|
+
return {
|
|
204
|
+
type: this._urls.robloxStudioAuthProtocol,
|
|
205
|
+
state: urlObj.searchParams.get("state") ?? undefined,
|
|
206
|
+
code: urlObj.searchParams.get("code") ?? undefined,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const isPlayer = url.startsWith(`${this._urls.robloxPlayerProtocol}:1`);
|
|
211
|
+
const isStudio = url.startsWith(`${this._urls.robloxStudioProtocol}:1`);
|
|
212
|
+
if (!isPlayer && !isStudio) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const split = url.split("+");
|
|
217
|
+
|
|
218
|
+
const type = isPlayer
|
|
219
|
+
? this._urls.robloxPlayerProtocol
|
|
220
|
+
: this._urls.robloxStudioProtocol;
|
|
221
|
+
|
|
222
|
+
const params: BuildAuthedProtocolUrlParameters<T> = {
|
|
223
|
+
type,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (let i = 1; i < split.length; i++) {
|
|
227
|
+
const item = split[i];
|
|
228
|
+
const [key, value] = item.split(":");
|
|
229
|
+
if (!value) continue;
|
|
230
|
+
|
|
231
|
+
switch (key.toLowerCase()) {
|
|
232
|
+
case "launchmode": {
|
|
233
|
+
params.launchMode = value as AuthedLaunchMode;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "gameinfo": {
|
|
237
|
+
params.gameInfo = value;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "launchtime": {
|
|
241
|
+
params.launchTime = value;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
default: {
|
|
245
|
+
params.otherParams ??= {};
|
|
246
|
+
params.otherParams[key] = decodeURIComponent(value);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return params;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public parseAuthedPlaceLauncherUrl(
|
|
255
|
+
urlStr: string,
|
|
256
|
+
): BuildAuthedPlaceLauncherURLParameters<U> | null {
|
|
257
|
+
try {
|
|
258
|
+
const url = new URL(urlStr);
|
|
259
|
+
|
|
260
|
+
const request = url.searchParams.get("request");
|
|
261
|
+
if (!request) return null;
|
|
262
|
+
|
|
263
|
+
const obj: BuildAuthedPlaceLauncherURLParameters<U> = {
|
|
264
|
+
request: request as BuildAuthedPlaceLauncherRequest,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
for (const [key, value] of url.searchParams) {
|
|
268
|
+
let valueToSet: number | boolean | string;
|
|
269
|
+
if (value === "true") {
|
|
270
|
+
valueToSet = true;
|
|
271
|
+
} else if (value === "false") {
|
|
272
|
+
valueToSet = false;
|
|
273
|
+
} else {
|
|
274
|
+
const parsedNumber = Number(value);
|
|
275
|
+
|
|
276
|
+
if (!Number.isNaN(parsedNumber)) {
|
|
277
|
+
valueToSet = parsedNumber;
|
|
278
|
+
} else {
|
|
279
|
+
valueToSet = value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// @ts-expect-error: Fine tbh
|
|
284
|
+
obj[key] = valueToSet;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return obj;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|