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
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import ParsedDeepLink, { type ExtractParameterType } from "./ParsedDeepLink";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_APPSYFLYER_BASE_URL,
|
|
4
|
+
DEFAULT_ROBLOX_PLAYER_DEEPLINK_PROTOCOL,
|
|
5
|
+
DEFAULT_ROBLOX_WEBSITE_URL,
|
|
6
|
+
DEFAULT_ROBLOX_API_DOMAIN,
|
|
7
|
+
LOCALE_REGEX,
|
|
8
|
+
} from "./utils/constants";
|
|
9
|
+
import { getDeepLinks, type DeepLink } from "./utils/deepLinks";
|
|
10
|
+
|
|
11
|
+
export type DeepLinkParserUrls = {
|
|
12
|
+
appsFlyerBaseUrl: string;
|
|
13
|
+
robloxPlayerDeepLinkProtocol: string;
|
|
14
|
+
robloxUrl: string;
|
|
15
|
+
robloxApiDomain: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DeepLinkParserFns = {
|
|
19
|
+
getPlaceUniverseId: (placeId: number) => Promise<number | null>;
|
|
20
|
+
getUniverseRootPlaceId: (placeId: number) => Promise<number | null>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DisallowedParams<
|
|
24
|
+
// biome-ignore lint/suspicious/noExplicitAny: A very strange typescript issue
|
|
25
|
+
T extends DeepLink<string, any, any, any>,
|
|
26
|
+
// biome-ignore lint/suspicious/noExplicitAny: A very strange typescript issue
|
|
27
|
+
> = T extends DeepLink<infer K, any, any, infer P>
|
|
28
|
+
? Record<K, Array<keyof P>>
|
|
29
|
+
: never;
|
|
30
|
+
|
|
31
|
+
export type DeepLinkParserConstructorProps<
|
|
32
|
+
// biome-ignore lint/suspicious/noExplicitAny: A very strange typescript issue
|
|
33
|
+
T extends DeepLink<string, any, any, any>,
|
|
34
|
+
> = {
|
|
35
|
+
urls?: Partial<DeepLinkParserUrls>;
|
|
36
|
+
fns?: Partial<DeepLinkParserFns>;
|
|
37
|
+
fetchFn?: typeof fetch;
|
|
38
|
+
disallowedParams?: DisallowedParams<T>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default class DeepLinkParser<
|
|
42
|
+
// biome-ignore lint/suspicious/noExplicitAny: A very strange typescript issue
|
|
43
|
+
T extends DeepLink<string, any, any, any> = ReturnType<
|
|
44
|
+
typeof getDeepLinks
|
|
45
|
+
>[number],
|
|
46
|
+
> {
|
|
47
|
+
public _urls: DeepLinkParserUrls = {
|
|
48
|
+
appsFlyerBaseUrl: DEFAULT_APPSYFLYER_BASE_URL,
|
|
49
|
+
robloxPlayerDeepLinkProtocol: DEFAULT_ROBLOX_PLAYER_DEEPLINK_PROTOCOL,
|
|
50
|
+
robloxUrl: DEFAULT_ROBLOX_WEBSITE_URL,
|
|
51
|
+
robloxApiDomain: DEFAULT_ROBLOX_API_DOMAIN,
|
|
52
|
+
};
|
|
53
|
+
private _fns: DeepLinkParserFns;
|
|
54
|
+
private _fetchFn: typeof fetch = fetch.bind(globalThis);
|
|
55
|
+
private _disallowedParams: DisallowedParams<T> | undefined;
|
|
56
|
+
|
|
57
|
+
public _deepLinks: T[];
|
|
58
|
+
|
|
59
|
+
constructor(data?: DeepLinkParserConstructorProps<T>) {
|
|
60
|
+
this._fns = {
|
|
61
|
+
getPlaceUniverseId:
|
|
62
|
+
data?.fns?.getPlaceUniverseId ??
|
|
63
|
+
((placeId) =>
|
|
64
|
+
this._fetchFn(
|
|
65
|
+
`https://apis${this._urls.robloxApiDomain}/universes/v1/places/${placeId}/universe`,
|
|
66
|
+
)
|
|
67
|
+
.then((res) => res.json())
|
|
68
|
+
.then((res) => res.universeId ?? null)),
|
|
69
|
+
getUniverseRootPlaceId:
|
|
70
|
+
data?.fns?.getUniverseRootPlaceId ??
|
|
71
|
+
((universeId) =>
|
|
72
|
+
this._fetchFn(
|
|
73
|
+
`https://games${this._urls.robloxApiDomain}/v1/games?universeIds=${universeId}`,
|
|
74
|
+
)
|
|
75
|
+
.then((res) => res.json())
|
|
76
|
+
.then((res) => res?.data?.[0]?.rootPlaceId ?? null)),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (data?.urls) {
|
|
80
|
+
for (const _key in data.urls) {
|
|
81
|
+
const key = _key as keyof DeepLinkParserUrls;
|
|
82
|
+
if (data.urls[key]) this._urls[key] = data.urls[key] as string;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (data?.disallowedParams) {
|
|
87
|
+
this._disallowedParams = data.disallowedParams;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (data?.fetchFn) this._fetchFn = data.fetchFn;
|
|
91
|
+
this._deepLinks = getDeepLinks(
|
|
92
|
+
this._fns.getUniverseRootPlaceId.bind(this),
|
|
93
|
+
this._fns.getPlaceUniverseId.bind(this),
|
|
94
|
+
this._urls.robloxUrl,
|
|
95
|
+
) as T[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public createDeepLink<U extends T["name"]>(
|
|
99
|
+
type: U,
|
|
100
|
+
params: ExtractParameterType<
|
|
101
|
+
Extract<
|
|
102
|
+
T,
|
|
103
|
+
{
|
|
104
|
+
name: U;
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
>["params"],
|
|
108
|
+
): ParsedDeepLink<
|
|
109
|
+
Extract<
|
|
110
|
+
T,
|
|
111
|
+
{
|
|
112
|
+
name: U;
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
> | null {
|
|
116
|
+
const deepLink = this._deepLinks.find((link) => link.name === type);
|
|
117
|
+
if (!deepLink) return null;
|
|
118
|
+
|
|
119
|
+
const validatedParams: Record<string, string> = {};
|
|
120
|
+
const requiredParams: Set<string> = new Set();
|
|
121
|
+
|
|
122
|
+
if (deepLink.protocolUrls) {
|
|
123
|
+
for (const url of deepLink.protocolUrls) {
|
|
124
|
+
if (url.path) {
|
|
125
|
+
for (const path of url.path) {
|
|
126
|
+
if (params[path.name]) {
|
|
127
|
+
// @ts-expect-error: fine
|
|
128
|
+
validatedParams[path.name] = String(params[path.name]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (url.query) {
|
|
133
|
+
for (const query of url.query) {
|
|
134
|
+
const paramName =
|
|
135
|
+
"mappedName" in query ? query.mappedName : query.name;
|
|
136
|
+
|
|
137
|
+
// Track required parameters
|
|
138
|
+
if (query.required === true) {
|
|
139
|
+
requiredParams.add(String(paramName));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate and add parameter if it exists
|
|
143
|
+
if (params[paramName] !== undefined) {
|
|
144
|
+
const value = String(params[paramName]);
|
|
145
|
+
// Check regex pattern if available
|
|
146
|
+
if (query.regex && !query.regex.test(value)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// @ts-expect-error: fine
|
|
151
|
+
validatedParams[paramName] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (deepLink.websiteUrls) {
|
|
159
|
+
for (const url of deepLink.websiteUrls) {
|
|
160
|
+
if (url.path) {
|
|
161
|
+
for (const path of url.path) {
|
|
162
|
+
if (params[path.name]) {
|
|
163
|
+
// @ts-expect-error: fine
|
|
164
|
+
validatedParams[path.name] = String(params[path.name]);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (url.query) {
|
|
169
|
+
for (const query of url.query) {
|
|
170
|
+
const paramName =
|
|
171
|
+
"mappedName" in query ? query.mappedName : query.name;
|
|
172
|
+
|
|
173
|
+
if (query.required === true) {
|
|
174
|
+
requiredParams.add(String(paramName));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (params[paramName] !== undefined) {
|
|
178
|
+
const value = String(params[paramName]);
|
|
179
|
+
// Check regex pattern if available
|
|
180
|
+
if (query.regex && !query.regex.test(value)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// @ts-expect-error: fine
|
|
185
|
+
validatedParams[paramName] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (deepLink.arbitaryParameters) {
|
|
193
|
+
for (const paramName in deepLink.arbitaryParameters) {
|
|
194
|
+
const allowed = deepLink.arbitaryParameters[paramName];
|
|
195
|
+
|
|
196
|
+
if (allowed && params[paramName] !== undefined) {
|
|
197
|
+
validatedParams[paramName] = String(params[paramName]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (this._disallowedParams?.[type]) {
|
|
203
|
+
for (const param of this._disallowedParams[type]) {
|
|
204
|
+
delete validatedParams[param as string];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const requiredParam of requiredParams) {
|
|
209
|
+
if (validatedParams[requiredParam] === undefined) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return new ParsedDeepLink<
|
|
215
|
+
Extract<
|
|
216
|
+
T,
|
|
217
|
+
{
|
|
218
|
+
name: U;
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
>(
|
|
222
|
+
{
|
|
223
|
+
type,
|
|
224
|
+
|
|
225
|
+
params: validatedParams,
|
|
226
|
+
// biome-ignore lint/suspicious/noExplicitAny: Fine
|
|
227
|
+
} as any,
|
|
228
|
+
this as DeepLinkParser,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public async parseAppsFlyerLink(
|
|
233
|
+
url: string,
|
|
234
|
+
): Promise<ParsedDeepLink<T> | null> {
|
|
235
|
+
if (!url.startsWith(`https://${this._urls.appsFlyerBaseUrl}`)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const urlObj = new URL(url);
|
|
239
|
+
|
|
240
|
+
const deepLinkMobile =
|
|
241
|
+
urlObj.searchParams.get("af_dp") ||
|
|
242
|
+
urlObj.searchParams.get("deep_link_value");
|
|
243
|
+
const deepLinkWeb = urlObj.searchParams.get("af_web_dp");
|
|
244
|
+
|
|
245
|
+
// prioritize mobile over web
|
|
246
|
+
if (deepLinkMobile) {
|
|
247
|
+
return this.parseProtocolLink(deepLinkMobile);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (deepLinkWeb) {
|
|
251
|
+
return this.parseWebsiteLink(deepLinkWeb);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async parseWebsiteLink(
|
|
258
|
+
url: string,
|
|
259
|
+
): Promise<ParsedDeepLink<T> | null> {
|
|
260
|
+
const urlObj = new URL(url);
|
|
261
|
+
let pathName = urlObj.pathname
|
|
262
|
+
.replace(/([^:]\/)\/+/g, "$1")
|
|
263
|
+
.replace(/\/$/, "");
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const split = pathName.split("/");
|
|
267
|
+
const localeMaybe = split[1];
|
|
268
|
+
if (
|
|
269
|
+
localeMaybe &&
|
|
270
|
+
LOCALE_REGEX.test(localeMaybe) &&
|
|
271
|
+
!["js", "my"].includes(localeMaybe)
|
|
272
|
+
) {
|
|
273
|
+
split.splice(1, 1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pathName = split.join("/");
|
|
277
|
+
} catch {
|
|
278
|
+
/* catch error */
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const searchParams = urlObj.searchParams;
|
|
282
|
+
|
|
283
|
+
for (const deepLink of this._deepLinks) {
|
|
284
|
+
if (!deepLink.websiteUrls) continue;
|
|
285
|
+
|
|
286
|
+
for (const url of deepLink.websiteUrls) {
|
|
287
|
+
const match = url.regex.exec(pathName);
|
|
288
|
+
if (!match) continue;
|
|
289
|
+
|
|
290
|
+
const requiredGroups: string[] = [];
|
|
291
|
+
let passing = true;
|
|
292
|
+
|
|
293
|
+
let params: Record<string, string> = {};
|
|
294
|
+
if (url.path) {
|
|
295
|
+
if (!match.groups) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const path of url.path) {
|
|
300
|
+
// @ts-expect-error: A very strange typescript issue
|
|
301
|
+
params[path.name] = match.groups?.[path.name];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (url.query) {
|
|
306
|
+
for (const search of url.query) {
|
|
307
|
+
// @ts-expect-error: A very strange typescript issue
|
|
308
|
+
const value = searchParams.get(search.name);
|
|
309
|
+
if (!value) {
|
|
310
|
+
if (
|
|
311
|
+
search.required === true ||
|
|
312
|
+
(typeof search.required === "string" &&
|
|
313
|
+
// @ts-expect-error: A very strange typescript issue
|
|
314
|
+
!requiredGroups.includes(search.name))
|
|
315
|
+
) {
|
|
316
|
+
passing = false;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (typeof search.required === "string") {
|
|
324
|
+
requiredGroups.push(search.required);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// @ts-expect-error: A very strange typescript issue
|
|
328
|
+
params["mappedName" in search ? search.mappedName : search.name] =
|
|
329
|
+
value;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!passing) continue;
|
|
334
|
+
if (deepLink.transformWebsiteParams) {
|
|
335
|
+
const transformedParams = await deepLink.transformWebsiteParams(
|
|
336
|
+
params,
|
|
337
|
+
urlObj,
|
|
338
|
+
);
|
|
339
|
+
if (!transformedParams) continue;
|
|
340
|
+
|
|
341
|
+
params = transformedParams;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (this._disallowedParams?.[deepLink.name]) {
|
|
345
|
+
for (const param of this._disallowedParams[deepLink.name]) {
|
|
346
|
+
delete params[param as string];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return new ParsedDeepLink<T>(
|
|
351
|
+
{
|
|
352
|
+
type: deepLink.name,
|
|
353
|
+
params,
|
|
354
|
+
// biome-ignore lint/suspicious/noExplicitAny: Fine
|
|
355
|
+
} as any,
|
|
356
|
+
this as DeepLinkParser,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
public async parseProtocolLink(
|
|
365
|
+
url: string,
|
|
366
|
+
): Promise<ParsedDeepLink<T> | null> {
|
|
367
|
+
const prepend = `${this._urls.robloxPlayerDeepLinkProtocol}://`;
|
|
368
|
+
const urlObj = new URL(url.replace(prepend, `${prepend}/`));
|
|
369
|
+
if (urlObj.protocol !== `${this._urls.robloxPlayerDeepLinkProtocol}:`) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
let searchParams = urlObj.searchParams;
|
|
373
|
+
|
|
374
|
+
const pathNameSplit = urlObj.pathname.split("/");
|
|
375
|
+
pathNameSplit.shift();
|
|
376
|
+
|
|
377
|
+
let pathName = pathNameSplit.join("/");
|
|
378
|
+
if (pathName.includes("=") && !urlObj.href.includes("?")) {
|
|
379
|
+
searchParams = new URLSearchParams(pathName);
|
|
380
|
+
pathName = "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const deepLink of this._deepLinks) {
|
|
384
|
+
if (!deepLink.protocolUrls) continue;
|
|
385
|
+
|
|
386
|
+
for (const url of deepLink.protocolUrls) {
|
|
387
|
+
const match = url.regex.exec(pathName);
|
|
388
|
+
if (!match) continue;
|
|
389
|
+
|
|
390
|
+
const requiredGroups: string[] = [];
|
|
391
|
+
let passing = true;
|
|
392
|
+
|
|
393
|
+
let params: Record<string, string> = {};
|
|
394
|
+
if (url.path) {
|
|
395
|
+
if (!match.groups) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const path of url.path) {
|
|
400
|
+
// @ts-expect-error: A very strange typescript issue
|
|
401
|
+
params[path.name] = match.groups[path.name];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (url.query) {
|
|
406
|
+
for (const search of url.query) {
|
|
407
|
+
// @ts-expect-error: A very strange typescript issue
|
|
408
|
+
const value = searchParams.get(search.name);
|
|
409
|
+
if (!value) {
|
|
410
|
+
if (
|
|
411
|
+
search.required === true ||
|
|
412
|
+
(typeof search.required === "string" &&
|
|
413
|
+
// @ts-expect-error: A very strange typescript issue
|
|
414
|
+
!requiredGroups.includes(search.name))
|
|
415
|
+
) {
|
|
416
|
+
passing = false;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (typeof search.required === "string") {
|
|
424
|
+
requiredGroups.push(search.required);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// @ts-expect-error: A very strange typescript issue
|
|
428
|
+
params["mappedName" in search ? search.mappedName : search.name] =
|
|
429
|
+
value;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (!passing) continue;
|
|
434
|
+
if (deepLink.transformProtocolParams) {
|
|
435
|
+
const transformedParams =
|
|
436
|
+
await deepLink.transformProtocolParams(params);
|
|
437
|
+
if (!transformedParams) continue;
|
|
438
|
+
|
|
439
|
+
params = transformedParams;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (this._disallowedParams?.[deepLink.name]) {
|
|
443
|
+
for (const param of this._disallowedParams[deepLink.name]) {
|
|
444
|
+
delete params[param as string];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return new ParsedDeepLink<T>(
|
|
449
|
+
{
|
|
450
|
+
type: deepLink.name,
|
|
451
|
+
params,
|
|
452
|
+
// biome-ignore lint/suspicious/noExplicitAny: Fine
|
|
453
|
+
} as any,
|
|
454
|
+
this as DeepLinkParser,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public parseLink(url: string): Promise<ParsedDeepLink<T> | null> {
|
|
463
|
+
if (url.startsWith(`https://${this._urls.appsFlyerBaseUrl}`)) {
|
|
464
|
+
return this.parseAppsFlyerLink(url);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (url.startsWith(`${this._urls.robloxPlayerDeepLinkProtocol}://`)) {
|
|
468
|
+
return this.parseProtocolLink(url);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return this.parseWebsiteLink(url);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type DeepLinkParser from "./DeepLinkParser";
|
|
2
|
+
import type { DeepLink } from "./utils/deepLinks";
|
|
3
|
+
|
|
4
|
+
export type ExtractParameterType<T> = T extends DeepLink<
|
|
5
|
+
infer U,
|
|
6
|
+
infer _V,
|
|
7
|
+
infer _W,
|
|
8
|
+
infer X
|
|
9
|
+
>
|
|
10
|
+
? {
|
|
11
|
+
type: U;
|
|
12
|
+
params: X;
|
|
13
|
+
}
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
export default class ParsedDeepLink<T extends DeepLink<string>> {
|
|
17
|
+
private _deepLink: T;
|
|
18
|
+
constructor(
|
|
19
|
+
public data: ExtractParameterType<T>,
|
|
20
|
+
private _deepLinkParser: DeepLinkParser,
|
|
21
|
+
) {
|
|
22
|
+
this._deepLink = (_deepLinkParser._deepLinks.find(
|
|
23
|
+
(link) => link.name === data.type,
|
|
24
|
+
) ?? {
|
|
25
|
+
name: data.type,
|
|
26
|
+
}) as T;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public toProtocolUrl(): string | null {
|
|
30
|
+
if (!this._deepLink.toProtocolUrl) return null;
|
|
31
|
+
|
|
32
|
+
const data = this.data;
|
|
33
|
+
let path =
|
|
34
|
+
typeof this._deepLink.toProtocolUrl === "function"
|
|
35
|
+
? this._deepLink.toProtocolUrl(data.params)
|
|
36
|
+
: this._deepLink.toProtocolUrl;
|
|
37
|
+
|
|
38
|
+
const search = new URLSearchParams();
|
|
39
|
+
|
|
40
|
+
for (const param in data.params) {
|
|
41
|
+
const check = this._deepLink.arbitaryParameters?.[param];
|
|
42
|
+
if (check && check !== "protocol") continue;
|
|
43
|
+
|
|
44
|
+
if (path.includes(`{${param}}`)) {
|
|
45
|
+
path = path.replace(
|
|
46
|
+
`{${param}}`,
|
|
47
|
+
data.params[param as keyof typeof data.params],
|
|
48
|
+
);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
search.append(param, data.params[param as keyof typeof data.params]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = new URL(
|
|
55
|
+
`${this._deepLinkParser._urls.robloxPlayerDeepLinkProtocol}://${path}`,
|
|
56
|
+
);
|
|
57
|
+
url.search = search.toString();
|
|
58
|
+
|
|
59
|
+
return url.toString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public toWebsiteUrl(): string | null {
|
|
63
|
+
if (!this._deepLink.toWebsiteUrl) return null;
|
|
64
|
+
|
|
65
|
+
const data = this.data;
|
|
66
|
+
let path =
|
|
67
|
+
typeof this._deepLink.toWebsiteUrl === "function"
|
|
68
|
+
? this._deepLink.toWebsiteUrl(data.params)
|
|
69
|
+
: this._deepLink.toWebsiteUrl;
|
|
70
|
+
|
|
71
|
+
const search = new URLSearchParams();
|
|
72
|
+
|
|
73
|
+
for (const param in data.params) {
|
|
74
|
+
const check = this._deepLink.arbitaryParameters?.[param];
|
|
75
|
+
if (check && check !== "website") continue;
|
|
76
|
+
|
|
77
|
+
if (path.includes(`{${param}}`)) {
|
|
78
|
+
path = path.replace(
|
|
79
|
+
`{${param}}`,
|
|
80
|
+
data.params[param as keyof typeof data.params],
|
|
81
|
+
);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
search.append(param, data.params[param as keyof typeof data.params]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const url = new URL(
|
|
88
|
+
`https://${this._deepLinkParser._urls.robloxUrl}${path}`,
|
|
89
|
+
);
|
|
90
|
+
url.search = search.toString();
|
|
91
|
+
|
|
92
|
+
return url.toString();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public toAppsFlyerUrl(): string | null {
|
|
96
|
+
const deepLinkMobile = this.toProtocolUrl();
|
|
97
|
+
const deepLinkWeb = this.toWebsiteUrl();
|
|
98
|
+
|
|
99
|
+
if (!deepLinkMobile && !deepLinkWeb) return null;
|
|
100
|
+
|
|
101
|
+
const url = new URL(
|
|
102
|
+
`https://${this._deepLinkParser._urls.appsFlyerBaseUrl}`,
|
|
103
|
+
);
|
|
104
|
+
if (deepLinkMobile) {
|
|
105
|
+
url.searchParams.append("af_dp", deepLinkMobile);
|
|
106
|
+
url.searchParams.append("deep_link_value", deepLinkMobile);
|
|
107
|
+
}
|
|
108
|
+
if (deepLinkWeb) {
|
|
109
|
+
url.searchParams.append("af_web_dp", deepLinkWeb);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return url.toString();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_APPSYFLYER_BASE_URL = "ro.blox.com/Ebh5";
|
|
2
|
+
export const DEFAULT_ROBLOX_PLAYER_DEEPLINK_PROTOCOL = "roblox";
|
|
3
|
+
export const DEFAULT_ROBLOX_WEBSITE_URL = "www.roblox.com";
|
|
4
|
+
export const DEFAULT_ROBLOX_API_DOMAIN = ".roblox.com";
|
|
5
|
+
export const UUID_REGEX =
|
|
6
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
7
|
+
export const LOCALE_REGEX = /^[a-z]{2}(\-[a-z0-9]{2,3})?$/i;
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_ROBLOX_PLAYER_AUTHED_PROTOCOL = "roblox-player";
|
|
10
|
+
export const DEFAULT_ROBLOX_STUDIO_AUTHED_PROTOCOL = "roblox-studio";
|
|
11
|
+
export const DEFAULT_ROBLOX_STUDIO_AUTH_AUTHED_PROTOCOL = "roblox-studio-auth";
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_PLACELAUNCHER_URL =
|
|
14
|
+
"https://www.roblox.com/game/PlaceLauncher.ashx";
|