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,8 @@
local Constants = {
SIGNALR_NAMESPACE = "TimedEntertainmentAllowanceNotifications",
SIGNALR_TYPE_NEW_INSTRUCTION = "NewInstruction",
HEARTBEAT_NOTIFICATIONS_NAMESPACE = "ScreenTimeClientNotifications",
HEARTBEAT_NOTIFICATION_TYPE_NEW_INSTRUCTION = "ScreentimeInstructionCheck",
}
return Constants
@@ -0,0 +1,5 @@
game:DefineFastFlag("LuaEnableScreenTime", false)
return function()
return game:GetFastFlag("LuaEnableScreenTime")
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("LuaEnableScreenTimeSignalR", false)
return function()
return game:GetFastFlag("LuaEnableScreenTimeSignalR")
end
@@ -0,0 +1,183 @@
--[[
Provides HTTP request methods for ScreenTime feature.
]]
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local UrlBuilder = require(CorePackages.Packages.UrlBuilder).UrlBuilder
local Logging = require(CorePackages.Logging)
local ArgCheck = require(CorePackages.ArgCheck)
local TEA_END_POINTS = {
GET_INSTRUCTIONS = "timed-entertainment-allowance/v1/instructions",
REPORT_EXECUTION = "timed-entertainment-allowance/v1/reportExecute",
}
local RESPONSE_FORMATS = {
GET_INSTRUCTIONS = {
errorCode = "number",
instructions = "table"
},
INSTRUCTION = {
type = "number",
instructionName = "string",
serialId = "string",
title = "string",
message = "string",
url = "string",
modalType = "number",
data = "string"
},
REPORT_EXECUTION = {
errorCode = "number",
},
}
local TAG = "HttpRequests"
local getInstructionsUrl = UrlBuilder.new({
base = Url.APIS_URL,
path = TEA_END_POINTS.GET_INSTRUCTIONS
})()
local reportExecutionUrl = UrlBuilder.new({
base = Url.APIS_URL,
path = TEA_END_POINTS.REPORT_EXECUTION
})()
--[[
A helper function to check object table have the required fields and fields'
type specified in format table.
It will throw exceptions, so must be encapsulated by pcall.
]]
local function checkFormat(format, object)
for key, typeString in pairs(format) do
assert(object[key] ~= nil, "Missing key")
assert(type(object[key]) == typeString, "Wrong type")
end
end
local HttpRequests = {
httpService = nil,
}
--[[
Create a new HttpRequests object.
@param httpService: Pass in HttpService from game:GetService("HttpService")
]]
function HttpRequests:new(httpService)
ArgCheck.isNotNil(httpService, "httpService")
local obj = {
httpService = httpService,
}
setmetatable(obj, self)
self.__index = self
return obj
end
--[[
Query TEA endpoint to get the instructions to execute.
@param callback: it should be non-nil with signature:
callback(success, unauthorized, instructions)
success (boolean): indicate whether the query is successful.
unauthorized (boolean): if success is false, indicate whether the
failure is due to authorization issue.
instructions (table): if success is true, this is the result
(associative array) of tables following
RESPONSE_FORMATS.INSTRUCTION format.
]]
function HttpRequests:getInstructions(callback)
ArgCheck.isNotNil(self.httpService, "httpService")
ArgCheck.isNotNil(callback, "callback")
local httpRequest = self.httpService:RequestInternal({
Url = getInstructionsUrl,
Method = "GET",
})
httpRequest:Start(function(reqSuccess, reqResponse)
local success
local err
local unauthorized = false
local instructions = {}
if not reqSuccess then
success = false
err = "Connection error"
elseif reqResponse.StatusCode == 401 then
success = false
unauthorized = true
err = "Unauthorized"
elseif reqResponse.StatusCode < 200 or reqResponse.StatusCode >= 400 then
success = false
err = "Status code: " .. reqResponse.StatusCode
else
-- reqSuccess == true and StatusCode >= 200 and StatusCode < 400
success, err = pcall(function()
local json = self.httpService:JSONDecode(reqResponse.Body)
checkFormat(RESPONSE_FORMATS.GET_INSTRUCTIONS, json)
assert(json.errorCode == 0, "Error code is not 0")
for i, instruction in ipairs(json.instructions) do
checkFormat(RESPONSE_FORMATS.INSTRUCTION, instruction)
end
instructions = json.instructions
end)
end
if not success then
Logging.warn(TAG .. " getInstructions failed: " .. getInstructionsUrl .. ", ".. err)
end
callback(success, unauthorized, instructions)
end)
end
--[[
Tell TEA endpoint that an instruction has been executed.
@param instructionName: from RESPONSE_FORMATS.INSTRUCTION
@param serialId: from RESPONSE_FORMATS.INSTRUCTION
@param callback: can be nil, signature: callback(success)
success (boolean): indicate whether the http request is successful.
]]
function HttpRequests:reportExecution(instructionName, serialId, callback)
ArgCheck.isNotNil(self.httpService, "httpService")
-- ISO 8601, Example: 2020-06-04T04:44:09Z
local formattedTime = os.date("%Y-%m-%dT%H:%M:%SZ")
local payload = self.httpService:JSONEncode({
instructionName = instructionName,
serialId = serialId,
execTime = formattedTime,
})
local httpRequest = self.httpService:RequestInternal({
Url = reportExecutionUrl,
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = payload,
})
httpRequest:Start(function(reqSuccess, reqResponse)
local success
local err
if not reqSuccess then
success = false
err = "Connection error"
elseif reqResponse.StatusCode < 200 and reqResponse.StatusCode >= 400 then
success = false
err = "Status code: " .. reqResponse.StatusCode
else
-- reqSuccess == true and StatusCode >= 200 and StatusCode < 400
success, err = pcall(function()
local json = self.httpService:JSONDecode(reqResponse.Body)
checkFormat(RESPONSE_FORMATS.REPORT_EXECUTION, json)
assert(json.errorCode == 0, "Error code is not 0")
end)
end
if not success then
Logging.warn(TAG .. " reportExecution failed: " .. reportExecutionUrl .. ", ".. err)
end
if callback ~= nil then
callback(success)
end
end)
end
return HttpRequests
@@ -0,0 +1,250 @@
local HttpRequests = require(script.Parent.HttpRequests)
function createMockHttpService(success, statusCode, errorCode, instructions)
local testBody = "test-body"
return {
RequestInternal = function(self, params)
return {
Start = function(self, callback)
local response = {
Body = testBody,
StatusCode = statusCode,
}
callback(success, response)
end
}
end,
JSONDecode = function(self, body)
assert(body == testBody)
return {
errorCode = errorCode,
instructions = instructions,
}
end,
JSONEncode = function(self, param)
return testBody
end,
}
end
return function()
local testInstructions = {{
type = 3,
instructionName = "name",
serialId = "id",
title = "title",
message = "message",
url = "url",
modalType = 0,
data = "",
}}
describe("getInstructions()", function()
it("should correctly callback when succeeded", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0, testInstructions))
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(true)
expect(unauthorized).to.equal(false)
expect(instructions).to.equal(testInstructions)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should throw when callback is nil", function()
local httpRequests = HttpRequests:new(createMockHttpService(false))
success, err = pcall(function()
httpRequests:getInstructions()
end)
expect(success).to.equal(false)
end)
it("should throw when not get from new", function()
called = false
function callback(success, unauthorized, instructions)
called = true;
end
success, err = pcall(function()
HttpRequests:getInstructions(callback)
end)
expect(success).to.equal(false)
expect(called).to.equal(false)
end)
it("should correctly callback when connection error", function()
local httpRequests = HttpRequests:new(createMockHttpService(false))
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should correctly callback when 401", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 401))
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
expect(unauthorized).to.equal(true)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should correctly callback when 412", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 412))
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
expect(unauthorized).to.equal(false)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should correctly callback when errorCode", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1))
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
expect(unauthorized).to.equal(false)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should correctly callback when decoding failed", function()
local httpService = createMockHttpService(true, 200, 0)
httpService.JSONDecode = function(self, body)
assert(false)
end
local httpRequests = HttpRequests:new(httpService)
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
expect(unauthorized).to.equal(false)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
it("should correctly callback when wrong response json format", function()
local httpService = createMockHttpService(true, 200, 0)
httpService.JSONDecode = function(self, body)
return {
errorCode = 0,
}
end
local httpRequests = HttpRequests:new(httpService)
local called = false
function callback(success, unauthorized, instructions)
expect(success).to.equal(false)
expect(unauthorized).to.equal(false)
called = true;
end
httpRequests:getInstructions(callback)
expect(called).to.equal(true)
end)
end)
describe("reportExecution()", function()
it("should correctly callback when succeeded", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0))
local called = false
function callback(success)
expect(success).to.equal(true)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
it("should throw when not get from new", function()
called = false
function callback(success)
called = true;
end
success, err = pcall(function()
HttpRequests:reportExecution("a", "b", callback)
end)
expect(success).to.equal(false)
expect(called).to.equal(false)
end)
it("should be ok with nil callback when succeeded", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 0))
httpRequests:reportExecution("a", "b", nil)
end)
it("should correctly callback when connection error", function()
local httpRequests = HttpRequests:new(createMockHttpService(false))
local called = false
function callback(success)
expect(success).to.equal(false)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
it("should correctly callback when 401", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 401))
local called = false
function callback(success)
expect(success).to.equal(false)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
it("should correctly callback when errorCode", function()
local httpRequests = HttpRequests:new(createMockHttpService(true, 200, 1))
local called = false
function callback(success)
expect(success).to.equal(false)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
it("should correctly callback when decoding failed", function()
local httpService = createMockHttpService(true, 200, 0)
httpService.JSONDecode = function(self, body)
assert(false)
end
local httpRequests = HttpRequests:new(httpService)
local called = false
function callback(success)
expect(success).to.equal(false)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
it("should correctly callback when wrong response json format", function()
local httpService = createMockHttpService(true, 200, 0)
httpService.JSONDecode = function(self, body)
return { }
end
local httpRequests = HttpRequests:new(httpService)
local called = false
function callback(success)
expect(success).to.equal(false)
called = true;
end
httpRequests:reportExecution("a", "b", callback)
expect(called).to.equal(true)
end)
end)
end
@@ -0,0 +1,59 @@
--[[
Provides utility methods for ScreenTime feature.
]]
local CorePackages = game:GetService("CorePackages")
local ArgCheck = require(CorePackages.ArgCheck)
local Logging = require(CorePackages.Logging)
local TAG = "Utils"
-- This global key will be accessed by native code
local GLOBAL_KEY_LOCKED_OUT = "ScreenTime.lockedOut"
-- This string will be checked against in native code (Java, etc.)
local TRUE_VALUE = "true"
local Utils = {
globalGetter = nil,
globalSetter = nil,
}
--[[
Create a new Utils object.
@param dependencies injected dependencies to get and set global key values.
The value should be accessible from native code.
members & interfaces:
globalGetter(string key) -> string
globalSetter(string key, string value)
]]
function Utils:new(dependencies)
ArgCheck.isType(dependencies.globalGetter, "function", "globalGetter")
ArgCheck.isType(dependencies.globalSetter, "function", "globalSetter")
local obj = {
globalGetter = dependencies.globalGetter,
globalSetter = dependencies.globalSetter,
}
setmetatable(obj, self)
self.__index = self
return obj
end
--[[
Get the flag whether current user is locked out.
]]
function Utils:isLockedOut()
ArgCheck.isType(self.globalGetter, "function", "globalGetter")
return (self.globalGetter(GLOBAL_KEY_LOCKED_OUT) == TRUE_VALUE)
end
--[[
Set the flag that current user is locked out.
It will be reset after successful MSDK login.
]]
function Utils:setLockedOut()
Logging.warn(TAG .. " setLockedOut")
ArgCheck.isType(self.globalSetter, "function", "globalSetter")
self.globalSetter(GLOBAL_KEY_LOCKED_OUT, TRUE_VALUE)
end
return Utils
@@ -0,0 +1,52 @@
local Utils = require(script.Parent.Utils)
function createMockDependencies()
local dict = {}
return {
globalGetter = function(key)
return dict[key]
end,
globalSetter = function(key, val)
dict[key] = val
end,
}
end
return function()
describe("new()", function()
it("should throw when getter is nil", function()
local mock = createMockDependencies()
mock.globalGetter = nil
success, err = pcall(function()
local utils = Utils:new(mock)
end)
expect(success).to.equal(false)
end)
it("should throw when setter is nil", function()
local mock = createMockDependencies()
mock.globalSetter = nil
success, err = pcall(function()
local utils = Utils:new(mock)
end)
expect(success).to.equal(false)
end)
end)
describe("isLockedOut()", function()
it("should return false before set", function()
local mock = createMockDependencies()
local utils = Utils:new(mock)
local flag = utils:isLockedOut()
expect(flag).to.equal(false)
end)
it("should return true after set", function()
local mock = createMockDependencies()
local utils = Utils:new(mock)
utils:setLockedOut()
local flag = utils:isLockedOut()
expect(flag).to.equal(true)
end)
end)
end