This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_string-utilities"]["string-utilities"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,9 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/url-builder"
version = "1.0.4"
commit = "cc593441e3efee555f0d6789c5df45138947909f"
source = "url+https://github.com/roblox/url-builder"
dependencies = [
"Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo",
"StringUtilities roblox/string-utilities 1.0.0 url+https://github.com/roblox/string-utilities",
]
@@ -0,0 +1,93 @@
local ContentProvider = game:GetService("ContentProvider")
--- base configuration -----------------------------
local baseUrl = ContentProvider.BaseUrl
baseUrl = string.gsub(baseUrl, ".*://", "")
baseUrl = string.gsub(baseUrl, "/.*", "")
baseUrl = string.gsub(baseUrl, "^www%.", "")
local UrlBase = {}
--[[
builds the base URL for an API
name (string): base name of the API, eg: "games"
params (optional table): a configuration table with any of the following:
* proto (optional string): the URL protocol, eg "http", "ftp", defaults to "https"
* version (optional int, string): an optional version, eg "1" will append "/v1" to the URL
* path (optional string): a subpath to append to the URL, no leading slash
* secure (optional boolean): equivalent to proto = "https"
all strings can be empty (including name), output will adapt accordingly
alternatively, version can be provided as second argument eg ("games", 1)
]]
function UrlBase.new(name, params)
assert(type(name) == "string", "UrlBase.new: `name` should be a string")
if params == nil then
params = {}
end
if type(params) == "number" or type(params) == "string" then
params = {version = params}
end
assert(type(params) == "table", "UrlBase.new: `params` should be a table")
local proto = params.proto
local version = params.version
local path = params.path
if proto == nil then
if params.secure == false then
proto = "http"
else
proto = "https"
end
end
local urlbase = proto
if #proto > 0 then
urlbase = urlbase .. "://"
end
urlbase = urlbase .. name
if #name > 0 then
urlbase = urlbase .. "."
end
urlbase = urlbase .. baseUrl
if version ~= nil and #tostring(version) > 0 then
urlbase = urlbase .. "/v" .. tostring(version)
end
if path ~= nil and #path > 0 then
urlbase = urlbase .. "/" .. path
end
return urlbase
end
local isQQ = string.sub(baseUrl, -6) == "qq.com"
-- from Url.lua
UrlBase.API = UrlBase.new("api")
UrlBase.APIS = UrlBase.new("apis")
UrlBase.AUTH = UrlBase.new("auth")
UrlBase.CHAT = UrlBase.new("chat")
UrlBase.FRIENDS = UrlBase.new("friends", 1)
UrlBase.ASSETGAME = UrlBase.new("assetgame")
UrlBase.GAMES = UrlBase.new("games", 1)
UrlBase.NOTIFICATION = UrlBase.new("notification", 2)
UrlBase.PRESENCE = UrlBase.new("presence", 1)
UrlBase.REALTIME = UrlBase.new("realtime")
UrlBase.WEB = UrlBase.new("web")
UrlBase.WWW = UrlBase.new("www")
UrlBase.ASD = UrlBase.new("ads", 1)
UrlBase.FOLLOWINGS = UrlBase.new("followings", 1)
UrlBase.PREMIUM = UrlBase.new("premiumfeatures", 1)
UrlBase.BLOG = "https://blog.roblox.com"
UrlBase.CORP = isQQ and "https://roblox.qq.com" or "https://corp.roblox.com"
-- from Http.lua
UrlBase.ACCOUNTSETTINGS = UrlBase.new("accountsettings")
UrlBase.BADGES = UrlBase.new("badges", 1)
UrlBase.INVENTORY = UrlBase.new("inventory", 1)
UrlBase.CATALOG = UrlBase.new("catalog", 1)
-- from AEWebApi.lua
UrlBase.AVATAR = UrlBase.new("avatar", 1)
UrlBase.MOBILENAV = "robloxmobile://navigation"
UrlBase.APPSFLYER = "https://ro.blox.com"
return UrlBase
@@ -0,0 +1,390 @@
local Packages = script.Parent.Parent
local Cryo = require(Packages.Cryo)
local StringUtilities = require(Packages.StringUtilities)
local StringTrim = StringUtilities.StringTrim
local StringSplit = StringUtilities.StringSplit
local encodeURIComponent = require(script.Parent.encodeURIComponent)
local UrlBase = require(script.Parent.UrlBase)
local GameUrlPatterns = require(script.Parent.UrlPatterns.GameUrlPatterns)
local UserUrlPatterns = require(script.Parent.UrlPatterns.UserUrlPatterns)
local StaticUrlPatterns = require(script.Parent.UrlPatterns.StaticUrlPatterns)
local CatalogUrlPatterns = require(script.Parent.UrlPatterns.CatalogUrlPatterns)
local UrlBuilder = {}
--[[
UTILITY FUNCTIONS
]]
-- splits by separator, trims whitspaces from each part and filters out empty ones
local function splitAndTrim(input, separator, limit)
local list = StringSplit(input, separator, limit)
list = Cryo.List.map(list, function(item)
return StringTrim(item)
end)
list = Cryo.List.filter(list, function(item)
return #item > 0
end)
return list
end
--[[
PATTERN VALIDATION
]]
-- a value should be a string, number, or a table of strings and numbers only
local function validateValueType(value)
local valueType = type(value)
if valueType == "string" then
return true
elseif valueType == "number" then
return true
elseif valueType == "table" then
for _, item in ipairs(value) do
local itemType = type(item)
if itemType ~= "string" and itemType ~= "number" then
return false
end
end
return true
else
return false
end
end
-- validate that item is either a literal (no check needed), or a valid placeholder
local function assertPlaceholder(element)
if string.sub(element, 1, 1) == "{" then
assert(string.sub(element, -1, -1) == "}", "invalid pattern: placeholder items should end with `}`")
end
end
-- validate a pattern's path or query element
local function assertElementIsValid(element, inQuery)
assert(type(element) == "table", "invalid pattern: elements should all be tables")
if inQuery then
assert(type(element.name) == "string", "invalid pattern: element name should be a string")
assert(#element.name > 0, "invalid pattern: element name should not be empty")
end
assert(
type(element.value) == "string" or type(element.value) == "number",
"invalid pattern: element value should be a string or number"
)
if type(element.value) == "string" then
assertPlaceholder(element.value)
end
if element.optional ~= nil then
assert(type(element.optional) == "boolean", "invalid pattern: element optional should be a boolean")
end
if element.default ~= nil then
assert(
validateValueType(element.default),
"invalid pattern: element default should be a string, number, or a table of strings and numbers only"
)
end
if element.collect ~= nil then
assert(
element.collect == "multi" or element.collect == "csv",
"invalid pattern: element optional should be one of `multi`, `csv`"
)
end
end
-- validate a full pattern object, could use a validator library in the future
local function assertPatternIsValid(pattern)
assert(type(pattern.base) == "string", "invalid pattern: base should be a string")
assert(#pattern.base > 0, "invalid pattern: base should not be empty")
assert(type(pattern.path) == "table", "invalid pattern: path should be a table")
for _, element in ipairs(pattern.path) do
assertElementIsValid(element, false)
end
if pattern.query ~= nil then
assert(type(pattern.query) == "table", "invalid pattern: query should be a table")
end
if type(pattern.query) == "table" then
for _, element in ipairs(pattern.query) do
assertElementIsValid(element, true)
end
end
if pattern.hash ~= nil then
assert(type(pattern.hash) == "string", "invalid pattern: hash should be a string")
end
end
--[[
STRING PATTERNS
]]
-- converts a "/" or "&" delimited string into a list or path/query elements
local function buildElementsFromString(elements, inQuery)
local separator = inQuery and "&" or "/"
local elements = splitAndTrim(elements, separator)
elements = Cryo.List.map(elements, function(element)
local elementName = nil
local elementValue = StringTrim(element)
local elementOptional = nil
local elementDefault = nil
local elementCollect = nil
if inQuery then
local queryitems = StringSplit(elementValue, "=", 2)
elementName = StringTrim(queryitems[1])
elementValue = queryitems[2]
if string.sub(elementName, 1, 1) == "{" then
elementName = StringTrim(elementName, "{}")
elementName = StringSplit(elementName, "|", 2)[1]
if elementValue == nil then
elementValue = queryitems[1]
end
elementName = StringTrim(elementName)
end
elementCollect = "multi"
end
if elementValue ~= nil and string.find(elementValue, "^{.*}$") then
elementValue = StringTrim(elementValue, "{}")
local valueitems = StringSplit(elementValue, "|", 2)
elementValue = StringTrim(valueitems[1])
if #valueitems > 1 then
elementOptional = true
if #(valueitems[2]) > 1 then
elementDefault = valueitems[2]
end
end
elementValue = "{" .. elementValue .. "}"
end
return {
name = elementName,
value = elementValue,
optional = elementOptional,
default = elementDefault,
collect = elementCollect,
}
end)
return elements
end
local function buildQueryStringFromTable(elements)
local stringpattern = {}
for name, value in pairs(elements) do
table.insert(stringpattern, name .. "=" .. value)
end
return table.concat(stringpattern, "&")
end
-- expands a pattern, replacing string parts with element tables
local function simplifyPattern(pattern)
local patternbase = pattern.base
local patternpath = pattern.path
local patternquery = pattern.query
local patternhash = pattern.hash
if patternbase ~= nil and UrlBase[string.upper(patternbase)] ~= nil then
patternbase = UrlBase[string.upper(patternbase)]
end
if type(patternpath) == "string" then
patternpath = buildElementsFromString(patternpath, false)
end
if type(patternquery) == "table" and patternquery[1] == nil then
patternquery = buildQueryStringFromTable(patternquery)
end
if type(patternquery) == "string" then
patternquery = buildElementsFromString(patternquery, true)
end
return {
base = patternbase,
path = patternpath,
query = patternquery,
hash = patternhash,
}
end
--[[
PATTERN RESOLUTION
]]
local function concatValues(elementValues, inQuery)
-- suppress all empty values, avoids double slashes and empty query params
elementValues = Cryo.List.filter(elementValues, function(value)
if inQuery then
return #splitAndTrim(value, "=") > 1
end
return #value > 0
end)
local separator = inQuery and "&" or "/"
return table.concat(elementValues, separator)
end
-- resolves the element to a string, from the given input table
local function resolveElement(element, input, inQuery)
if input == nil then
input = {}
end
local elementValues
if type(element.value) == "string" and string.sub(element.value, 1, 1) == "{" then
elementValues = input[string.sub(element.value, 2, -2)]
if elementValues == nil then
elementValues = element.default
end
if not element.optional then
assert(elementValues ~= nil, "UrlBuilder: missing parameter: `" .. element.value .. "`")
end
if elementValues == nil then
-- at this point optional == true, so we remove the element from output
elementValues = {}
end
assert(
validateValueType(elementValues),
"UrlBuilder: invalid parameter: `" .. element.value .. "`, " ..
"should be a string, number, or a table of strings and numbers only"
)
else
elementValues = element.value
end
if type(elementValues) ~= "table" then
elementValues = {elementValues}
end
elementValues = Cryo.List.map(elementValues, function(value)
return encodeURIComponent(tostring(value))
end)
if inQuery then
-- we are resolving a query parameter
if element.collect == "csv" then
elementValues = {table.concat(elementValues, "%2C")} -- ","
end
elementValues = Cryo.List.map(elementValues, function(value)
return encodeURIComponent(element.name) .. "=" .. value
end)
end
return concatValues(elementValues, inQuery)
end
local function resolveElementList(elementList, input, inQuery)
local elementValues = Cryo.List.map(elementList or {}, function(element)
return resolveElement(element, input, inQuery)
end)
return concatValues(elementValues, inQuery)
end
--[[
PATTERN CONSTRUCTION
]]
--[[
creates a new URL builder function for the given pattern
the function can then be called with an input (table) to generate a URL
pattern (table): pattern specification with the following:
* base (string): the domain and base path for the URL, eg "http://static.roblox.com"
* the UrlBase module exposes a list of available APIs, or can be used to properly build a new base
* path (string or table): list of path elements, one of
* (string): "/" delimited string, each element can be a literal or a "{}" enclosed placeholder
* (table): list of full path elements, see details below
* query (optional table): list of querystring elements, one of
* (string) : query string, with optional placeholders, eg "imageId={images}&size=140"
* (table): dictionary of {name = value, ...} pairs, eg {imageId = "{images}", size = 140}
* (table): list of full query elements, see details below
* hash (optional string): will be appended AS IS (no placeholders or UrlEncode), separated by "#"
path and query elements are tables with the following (some only apply to query elements):
* name (string): *query only* the name of the query parameter
* value (string or number): value of the element, can be a literal or a "{}" enclosed placeholder
* optional (optional boolean): marks the element as optional, if placeholder value can't be found
* default (optional string): value if placeholder can't be found, implies "optional = true"
* collect (optional string): *query only* how to resolve table values, one of
* "multi": param and value will be repeated, eg "p=v1&p=v2&p=v3", this is the default
* "csv": one param, values will be concatenated, eg "p=v1,v2,v3"
]]
function UrlBuilder.new(pattern)
pattern = simplifyPattern(pattern)
assertPatternIsValid(pattern)
return function(input, expected)
local url = StringTrim(pattern.base, "/", {right = true})
local path = resolveElementList(pattern.path, input, false)
if #path > 0 then
url = url .. "/" .. path
end
-- append slash if URL only consists of "proto://domain"
if string.match(url, "[^/]/[^/]") == nil then
url = url .. "/"
end
local query = resolveElementList(pattern.query, input, true)
if #query > 0 then
url = url .. "?" .. query
end
if pattern.hash and #pattern.hash > 0 then
url = url .. "#" .. pattern.hash
end
-- testing, for development use only
if expected then
if url ~= expected then
warn("UrlBuilder: unexpected output for pattern:")
warn("UrlBuilder: expected `" .. expected .. "`")
warn("UrlBuilder: actual `" .. url .. "`")
end
return expected
end
return url
end
end
--[[
creates a new URL builder function from a string pattern
pattern format: "base:path/to/{endpoint}?param1={value1}&{param2}"
]]
function UrlBuilder.fromString(pattern)
local patternitems = StringSplit(pattern, ":", 2)
if #patternitems < 2 then
patternitems = {"", patternitems[1]}
end
local patternbase = patternitems[1]
patternitems = StringSplit(patternitems[2], "%#", 2)
local patternhash = patternitems[2] or ""
patternitems = StringSplit(patternitems[1], "%?", 2)
local patternpath = patternitems[1] or ""
local patternquery = patternitems[2] or ""
-- in case ":" was the protocol delimiter of a full url (eg http://domain/...)
if string.sub(patternpath, 1, 2) == "//" then
patternitems = StringSplit(string.sub(patternpath, 3), "/", 2)
patternbase = patternbase .. "://" .. patternitems[1]
patternpath = patternitems[2] or "/"
end
return UrlBuilder.new({
base = StringTrim(patternbase),
path = StringTrim(patternpath),
query = StringTrim(patternquery),
hash = patternhash,
})
end
--[[
CONVENIENCE SHORTHANDS
]]
function UrlBuilder.addQueryString(url, query)
local pattern = StringTrim(url)
local queryindex = string.find(pattern, "%?")
if queryindex == nil then
pattern = pattern .. "?"
elseif queryindex < #pattern then
pattern = pattern .. "&"
end
local queryitems = Cryo.Dictionary.keys(query)
queryitems = Cryo.List.map(queryitems, function(param)
return "{" .. param .. "}"
end)
queryitems = table.concat(queryitems, "&")
pattern = pattern .. queryitems
return UrlBuilder.fromString(pattern)(query)
end
--[[
PATTERN REGISTRATION
]]
UrlBuilder.game = GameUrlPatterns(UrlBuilder)
UrlBuilder.user = UserUrlPatterns(UrlBuilder)
UrlBuilder.catalog = CatalogUrlPatterns(UrlBuilder)
UrlBuilder.static = StaticUrlPatterns(UrlBuilder)
return UrlBuilder
@@ -0,0 +1,85 @@
return function()
local UrlBuilder = require(script.Parent.UrlBuilder)
describe("simple", function()
local pattern = UrlBuilder.fromString("api:game/{universeId}/thumbnail?size={pxWidth|150}")
it("should generate proper url", function()
local url = pattern({
universeId = "1356984689",
pxWidth = 720,
})
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=720")
end)
it("table values duplicate url parts", function()
local url = pattern({
universeId = {"1356984689", "8745654337"},
pxWidth = {720, 320},
})
expect(url).to.equal("https://api.roblox.com/game/1356984689/8745654337/thumbnail?size=720&size=320")
end)
it("empty table should remove url part", function()
local url = pattern({
universeId = "1356984689",
pxWidth = {},
})
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail")
end)
it("missing values should throw", function()
local function getUrl()
pattern({pxWidth = 720})
end
expect(getUrl).to.throw()
end)
it("missing optional values should not throw", function()
local url = pattern({universeId = "1356984689"})
expect(url).to.equal("https://api.roblox.com/game/1356984689/thumbnail?size=150")
end)
end)
describe("complex", function()
local pattern = UrlBuilder.fromString(" https://roblox.com/ test/{special}/path / // {empty}/{multiple}/ x ? {num}&param1=static&dupl={multiple}&")
it("complex pattern/values with nultiple edge cases", function()
local url = pattern({
special = "$pec!al",
num = 568,
multiple = {"ab/", "c d"},
empty = "",
})
expect(url).to.equal("https://roblox.com/test/%24pec!al/path/ab%2F/c%20d/x?num=568&param1=static&dupl=ab%2F&dupl=c%20d")
end)
end)
describe("invalid patterns", function()
it("should throw on malformed placeholder", function()
local function invalidPattern()
UrlBuilder.fromString("api:game/{universeI}d/thumbnail?size={pxWidth|150}")
end
expect(invalidPattern).to.throw()
end)
it("should throw on missing base", function()
local function invalidPattern()
UrlBuilder.fromString("/game/{universeId}/thumbnail?size={pxWidth|150}")
end
expect(invalidPattern).to.throw()
end)
it("should throw on missing parameter name", function()
local function invalidPattern()
UrlBuilder.fromString("api:game/{universeId}/thumbnail?={pxWidth|150}")
end
expect(invalidPattern).to.throw()
end)
end)
end
@@ -0,0 +1,7 @@
{
"lint": {
"LocalShadow": "fatal",
"LocalUnused": "fatal",
"ImportUnused": "fatal"
}
}
@@ -0,0 +1,29 @@
--!nocheck
return function(UrlBuilder)
local CatalogUrlPatterns = {}
CatalogUrlPatterns.info = {
webbundle = UrlBuilder.fromString("www:bundles/{assetId}"),
webasset = UrlBuilder.fromString("www:catalog/{assetId}"),
webpage = function(params)
if params.assetType == "Bundle" then
return CatalogUrlPatterns.info.webbundle(params)
elseif params.assetType == "Asset" then
return CatalogUrlPatterns.info.webasset(params)
else
warn(string.format("%s - unknown assetType of %s", tostring(script.name), tostring(params.assetType)))
return nil
end
end,
appsflyer = function(params)
return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({
mobileUrl = UrlBuilder.fromString("mobilenav:item_details?itemId={assetId}&itemType={assetType}")(params),
webUrl = CatalogUrlPatterns.info.webpage(params),
})
end,
}
return CatalogUrlPatterns
end
@@ -0,0 +1,25 @@
return function()
local UrlBuilder = require(script.Parent.Parent.UrlBuilder)
describe("appsflyer link", function()
it("should generate proper bundle url", function()
local url = UrlBuilder.catalog.info.appsflyer({
assetId = "1356984689",
assetType = "Bundle",
})
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DBundle" ..
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fbundles%2F1356984689")
end)
it("should generate proper asset url", function()
local url = UrlBuilder.catalog.info.appsflyer({
assetId = "1356984689",
assetType = "Asset",
})
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fitem_details%3FitemId%3D1356984689%26itemType%3DAsset" ..
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fcatalog%2F1356984689")
end)
end)
end
@@ -0,0 +1,59 @@
--!nocheck
return function(UrlBuilder)
local GameUrlPatterns = {}
--- from GameInfoList.lua
GameUrlPatterns.info = {
webpage = UrlBuilder.fromString("www:games/{placeId}"),
store = UrlBuilder.fromString("www:games/store-section/{universeId}"),
badges = UrlBuilder.fromString("www:games/badges-section/{universeId}"),
servers = UrlBuilder.fromString("www:games/servers-section/{universeId}"),
serversPreopenCreateVip = UrlBuilder.fromString("www:games/servers-section-preopen-create-vip/{universeId}"),
group = UrlBuilder.fromString("www:groups/{creatorId}"),
user = UrlBuilder.fromString("www:users/{creatorId}/profile"),
-- {creatorType=Group|User, creatorId}
creator = function(params)
if params.creatorType == "Group" then
return GameUrlPatterns.info.group(params)
elseif params.creatorType == "User" then
return GameUrlPatterns.info.user(params)
end
warn(string.format("%s - unknown creatorType of %s", tostring(script.name), tostring(params.creatorType)))
return nil
end,
appsflyer = function(params)
return UrlBuilder.fromString("appsflyer:Ebh5?pid=share&is_retargeting=true&af_dp={mobileUrl}&af_web_dp={webUrl}")({
mobileUrl = UrlBuilder.fromString("mobilenav:game_details?gameId={universeId}")(params),
webUrl = GameUrlPatterns.info.webpage(params),
})
end,
}
--- from Http/Requests/*
GameUrlPatterns.details = UrlBuilder.fromString("games:games?{universeIds}")
GameUrlPatterns.playability = UrlBuilder.fromString("games:games/multiget-playability-status?{universeIds}")
GameUrlPatterns.media = UrlBuilder.fromString("games:games/{universeId}/media")
GameUrlPatterns.favorite = UrlBuilder.fromString("games:games/{universeId}/favorites")
GameUrlPatterns.social = UrlBuilder.fromString("games:games/{universeId}/social-links/list")
GameUrlPatterns.recommended = UrlBuilder.fromString("games:games/recommendations/game/{universeId}?{paginationKey|}&{maxRows|6}")
GameUrlPatterns.thumbnail = UrlBuilder.fromString("games:games/game-thumbnails?{height|150}&{width|150}&{imageTokens}")
GameUrlPatterns.vote = {
-- votes for all users
all = UrlBuilder.fromString("games:games/{universeId}/votes"),
-- current user vote status
get = UrlBuilder.fromString("games:games/{universeId}/votes/user"),
set = UrlBuilder.fromString("games:games/{universeId}/user-votes"),
}
GameUrlPatterns.follow = {
get = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}/status"),
set = UrlBuilder.fromString("followings:users/{userId}/universes/{universeId}"),
}
GameUrlPatterns.report = UrlBuilder.fromString("www:abusereport/asset?id={placeId}")
GameUrlPatterns.place = UrlBuilder.fromString("games:games/multiget-place-details?{placeIds}")
return GameUrlPatterns
end
@@ -0,0 +1,15 @@
return function()
local UrlBuilder = require(script.Parent.Parent.UrlBuilder)
describe("appsflyer link", function()
it("should generate proper url", function()
local url = UrlBuilder.game.info.appsflyer({
universeId = "1356984689",
placeId = "123456789",
})
expect(url).to.equal("https://ro.blox.com/Ebh5?pid=share&is_retargeting=true" ..
"&af_dp=robloxmobile%3A%2F%2Fnavigation%2Fgame_details%3FgameId%3D1356984689" ..
"&af_web_dp=https%3A%2F%2Fwww.roblox.com%2Fgames%2F123456789")
end)
end)
end
@@ -0,0 +1,55 @@
return function(UrlBuilder)
local function isQQ()
return string.find(UrlBuilder.fromString("corp:")(), "qq.com")
end
return {
catalog = UrlBuilder.fromString("www:catalog"),
buildersClub = UrlBuilder.fromString("www:mobile-app-upgrades/native-ios/bc"),
trades = UrlBuilder.fromString("www:trades"),
profile = UrlBuilder.fromString("www:users/profile"),
friends = UrlBuilder.fromString("www:users/friends"),
groups = UrlBuilder.fromString("www:my/groups"),
inventory = UrlBuilder.fromString("www:users/inventory"),
messages = UrlBuilder.fromString("www:my/messages"),
feed = UrlBuilder.fromString("www:feeds/inapp"),
develop = UrlBuilder.fromString("www:develop/landing"),
blog = UrlBuilder.fromString("blog:"),
help = UrlBuilder.fromString(isQQ() and "corp:faq" or "www:help"),
email = {
getSetEmail = UrlBuilder.fromString("accountSettings:v1/email"),
sendVerificationEmail = UrlBuilder.fromString("accountSettings:v1/email/verify")
},
about = {
us = UrlBuilder.fromString("corp:"),
careers = UrlBuilder.fromString(isQQ() and "corp:careers.html" or "corp:careers"),
parents = UrlBuilder.fromString("corp:parents"),
terms = function(params)
if isQQ() and params.useGameQQUrls then
return UrlBuilder.fromString("https://game.qq.com/contract.shtml")()
else
return UrlBuilder.fromString("www:info/terms")()
end
end,
privacy = function(params)
if isQQ() and params.useGameQQUrls then
return UrlBuilder.fromString("https://game.qq.com/privacy_guide.shtml")()
else
return UrlBuilder.fromString("www:info/privacy")()
end
end,
},
settings = {
account = UrlBuilder.fromString("www:my/account#!/info"),
security = UrlBuilder.fromString("www:my/account#!/security"),
privacy = UrlBuilder.fromString("www:my/account#!/privacy"),
billing = UrlBuilder.fromString("www:my/account#!/billing"),
notifications = UrlBuilder.fromString("www:my/account#!/notifications"),
},
tencent = {
childrenPrivacyGuide = UrlBuilder.fromString("https://game.qq.com/privacy_guide_children.shtml"),
luobuRiderTerms = UrlBuilder.fromString("https://roblox.qq.com/web201904/newsdetail.html?newsid=12429812"),
reputationInfo = UrlBuilder.fromString("https://gamecredit.qq.com/static/games/index.htm"),
},
}
end
@@ -0,0 +1,22 @@
return function(UrlBuilder)
return {
profile = UrlBuilder.fromString("www:users/{userId}/profile"),
group = UrlBuilder.fromString("www:groups/{groupId}/{groupName|}#!/about"),
friends = UrlBuilder.fromString("www:users/{userId}/friends"),
inventory = UrlBuilder.fromString("www:users/{userId}/inventory"),
search = UrlBuilder.fromString("www:search/users?{keyword}"),
report = function(params)
-- Web is fixing a bug that requires actionName and redirectUrl for this page to work
-- once fixed, this pattern function can be replaced with
-- UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{conversationId}"),
return UrlBuilder.fromString("www:abusereport/embedded/chat?id={userId}&{actionName}&{conversationId}&{redirecturl}")({
userId = params.userId,
conversationId = params.conversationId,
actionName = "chat",
redirecturl = UrlBuilder.fromString("www:home")(),
})
end
}
end
@@ -0,0 +1,7 @@
local function formatCharacter(character)
return string.format("%%%02X", character:byte(1,1))
end
return function(stringToEncode)
return stringToEncode:gsub("[^%w_%-%!%.%~%*%'%(%)]", formatCharacter)
end
@@ -0,0 +1,18 @@
return function()
local encodeURIComponent = require(script.Parent.encodeURIComponent)
it('should not filter alphanumerics', function()
local str = 'abcXYZ1230'
expect(encodeURIComponent(str)).to.equal(str)
end)
it('should not filter allowed special characters', function()
local str = 'abcABC123-_.!~*\'()'
expect(encodeURIComponent(str)).to.equal(str)
end)
it('should filter other non-alphanumeric characters', function()
local str = 'hello world&result=true'
expect(encodeURIComponent(str)).to.equal('hello%20world%26result%3Dtrue')
end)
end
@@ -0,0 +1,5 @@
return {
encodeURIComponent = require(script.encodeURIComponent),
UrlBase = require(script.UrlBase),
UrlBuilder = require(script.UrlBuilder),
}