@rbxts/zyntex-sdk 6.0.0 → 6.0.1
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/package.json +1 -1
- package/src/Experiments.luau +0 -114
- package/src/MainModule.luau +0 -23
- package/src/Telemetry.luau +0 -176
- package/src/Zyntex.luau +0 -1631
- package/src/api.luau +0 -237
- package/src/index.d.ts +0 -26
- package/src/init.luau +0 -26
- package/src/types.luau +0 -21
- package/src/zyntex.client.luau +0 -6
package/src/Zyntex.luau
DELETED
|
@@ -1,1631 +0,0 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
Zyntex Roblox SDK
|
|
3
|
-
Version: 6
|
|
4
|
-
Last Updated: 2025-09-01
|
|
5
|
-
Author: Deluct
|
|
6
|
-
|
|
7
|
-
This module is the main client for the Zyntex Advanced Admin/Ops Panel.
|
|
8
|
-
It handles communication with the Zyntex API, manages server status, player events,
|
|
9
|
-
and listens for administrative actions from the Zyntex dashboard.
|
|
10
|
-
|
|
11
|
-
For full documentation, visit: https://docs.zyntex.dev/
|
|
12
|
-
]]
|
|
13
|
-
|
|
14
|
-
local Stats = game:GetService("Stats")
|
|
15
|
-
local RunService = game:GetService("RunService")
|
|
16
|
-
local Players = game:GetService("Players")
|
|
17
|
-
local LogService = game:GetService("LogService")
|
|
18
|
-
local Chat = game:GetService("Chat")
|
|
19
|
-
local TCS = game:GetService("TextChatService")
|
|
20
|
-
local MS = game:GetService("MarketplaceService")
|
|
21
|
-
|
|
22
|
-
local API = require(script.Parent:FindFirstChild("api"))
|
|
23
|
-
local TYPES = require(script.Parent:FindFirstChild("types"))
|
|
24
|
-
local Telemetry = require(script.Parent:FindFirstChild("Telemetry"))
|
|
25
|
-
local Experiments = require(script.Parent:FindFirstChild("Experiments"))
|
|
26
|
-
|
|
27
|
-
local CLIENT_VERSION = 6
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
--[[
|
|
32
|
-
@function now
|
|
33
|
-
@return string
|
|
34
|
-
|
|
35
|
-
Generates a UTC timestamp in ISO 8601 format. This is used for 'since'
|
|
36
|
-
parameters in API calls to ensure all data is synchronized correctly.
|
|
37
|
-
]]
|
|
38
|
-
local function now()
|
|
39
|
-
local dt = DateTime.now():ToUniversalTime()
|
|
40
|
-
return string.format(
|
|
41
|
-
"%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
|
|
42
|
-
dt.Year, dt.Month, dt.Day,
|
|
43
|
-
dt.Hour, dt.Minute, dt.Second,
|
|
44
|
-
dt.Millisecond
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
--[[
|
|
49
|
-
@function toFormat
|
|
50
|
-
@param date DateTime
|
|
51
|
-
@return string
|
|
52
|
-
|
|
53
|
-
Converts a given Roblox DateTime object into a UTC timestamp in ISO 8601 format.
|
|
54
|
-
]]
|
|
55
|
-
local function toFormat(date: DateTime)
|
|
56
|
-
local date = date:ToUniversalTime()
|
|
57
|
-
return string.format(
|
|
58
|
-
"%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
|
|
59
|
-
date.Year, date.Month, date.Day,
|
|
60
|
-
date.Hour, date.Minute, date.Second,
|
|
61
|
-
date.Millisecond
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
--[[
|
|
66
|
-
https://devforum.roblox.com/t/mute-players-with-textchatservice/2519300/4?u=deluc_t
|
|
67
|
-
]]
|
|
68
|
-
local function muteUserId(mutedUserId)
|
|
69
|
-
-- listen for future TextSources
|
|
70
|
-
TCS.DescendantAdded:Connect(function(child)
|
|
71
|
-
if child:IsA("TextSource") then
|
|
72
|
-
if child.UserId == mutedUserId then
|
|
73
|
-
child:Destroy()
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end)
|
|
77
|
-
|
|
78
|
-
-- mute any current TextSources
|
|
79
|
-
for _, child in TCS:GetDescendants() do
|
|
80
|
-
if child:IsA("TextSource") then
|
|
81
|
-
if child.UserId == mutedUserId then
|
|
82
|
-
child:Destroy()
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events"):FindFirstChild("SystemChat"):FireClient(Players:GetPlayerByUserId(mutedUserId), `You're muted.`)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
--[[
|
|
91
|
-
_____ _
|
|
92
|
-
| __ \| |
|
|
93
|
-
| |__) | | __ _ _ _ ___ _ __
|
|
94
|
-
| ___/| |/ _` | | | |/ _ \ '__|
|
|
95
|
-
| | | | (_| | |_| | __/ |
|
|
96
|
-
|_| |_|\__,_|\__, |\___|_|
|
|
97
|
-
__/ |
|
|
98
|
-
|___/
|
|
99
|
-
|
|
100
|
-
This section defines the 'ZyntexPlayer' object, which represents a Player
|
|
101
|
-
in the Zyntex dashboard.
|
|
102
|
-
]]
|
|
103
|
-
|
|
104
|
-
local ZyntexPlayer = {}
|
|
105
|
-
ZyntexPlayer.__index = ZyntexPlayer
|
|
106
|
-
|
|
107
|
-
-- Represents a single, recorded instance of an invoked event.
|
|
108
|
-
type ZyntexPlayerType = {
|
|
109
|
-
id: number; -- The unique ID of this specific player. Equivalent to the Roblox UserId
|
|
110
|
-
name: string; -- The player's username as it appears on Roblox.
|
|
111
|
-
avatar_url: string; -- The player's headshot avatar url.
|
|
112
|
-
avatar_url_cache_expiry: string; -- When the player's avatar URL expires. Not important for Roblox.
|
|
113
|
-
reputation: string; -- The player's reputation enum. Can be: 'clean', 'suspect', or 'offender'. https://docs.zyntex.dev/moderation/reputation
|
|
114
|
-
raw_reputation: number; -- The player's raw reputation as an integer. https://docs.zyntex.dev/moderation/reputation
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export type ZyntexPlayer = typeof(setmetatable({} :: ZyntexPlayerType, ZyntexPlayer))
|
|
118
|
-
|
|
119
|
-
function ZyntexPlayer.new(id: number, name: string, avatar_url: string, avatar_url_cache_expiry: string, reputation: string, raw_reputation: number)
|
|
120
|
-
local self = {}
|
|
121
|
-
self.id = id;
|
|
122
|
-
self.name = name
|
|
123
|
-
self.avatar_url = avatar_url
|
|
124
|
-
self.avatar_url_cache_expiry = avatar_url_cache_expiry
|
|
125
|
-
self.reputation = reputation
|
|
126
|
-
self.raw_reputation = raw_reputation
|
|
127
|
-
|
|
128
|
-
return setmetatable(self, ZyntexPlayer)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
local SERVER_ACTIVITY = {} -- stores player joins & leaves to send to Zyntex in the case that the servers go down during the server
|
|
132
|
-
local sendingStatus = false
|
|
133
|
-
|
|
134
|
-
--[[
|
|
135
|
-
|
|
136
|
-
_____ _ _
|
|
137
|
-
|_ _| | | (_)
|
|
138
|
-
| | _ ____ _____ ___ __ _ | |_ _ ___ _ __ ___
|
|
139
|
-
| | | '_ \ \ / / _ \ / __/ _` | __| |/ _ \| '_ \ / __|
|
|
140
|
-
_| |_| | | \ V / (_) | (_| (_| | |_| | (_) | | | \__ \
|
|
141
|
-
|_____|_| |_|\_/ \___/ \___\__,_|\__|_|\___/|_| |_|___/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
This section defines the 'Invocation' object, which represents a single,
|
|
146
|
-
successful execution of an Event.
|
|
147
|
-
]]
|
|
148
|
-
|
|
149
|
-
export type Data = {{["key"]: string, ["value"]: any}}
|
|
150
|
-
export type DataSchema = {{["key"]: string, ["type"]: string}}
|
|
151
|
-
|
|
152
|
-
local Invocation = {}
|
|
153
|
-
Invocation.__index = Invocation
|
|
154
|
-
|
|
155
|
-
-- Represents a single, recorded instance of an invoked event.
|
|
156
|
-
type InvocationType = {
|
|
157
|
-
id: number; -- The unique ID of this specific invocation.
|
|
158
|
-
eventId: number; -- The ID of the Event that was invoked.
|
|
159
|
-
data: Data; -- The data payload that was sent with the invocation.
|
|
160
|
-
to: string; -- The destination of the invocation (e.g., a specific server or 'global').
|
|
161
|
-
sender: string; -- Who or what sent the invocation (e.g., 'dashboard', 'server').
|
|
162
|
-
fromServer: string; -- The ID of the server that sent the invocation, if applicable.
|
|
163
|
-
invokedAt: string; -- The ISO 8601 timestamp of when the invocation occurred.
|
|
164
|
-
toServer: string; -- The ID of the specific server this was sent to, if applicable.
|
|
165
|
-
gameId: number; -- The Roblox Place ID where the invocation originated.
|
|
166
|
-
invokedBy: number; -- The UserID of the admin who triggered the invocation from the dashboard, if applicable.
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export type Invocation = typeof(setmetatable({} :: InvocationType, Invocation))
|
|
170
|
-
|
|
171
|
-
--[[
|
|
172
|
-
@constructor Invocation.new
|
|
173
|
-
@description Constructs a new Invocation object from raw data. This is typically used internally
|
|
174
|
-
by the client to wrap data received from the API into a usable object.
|
|
175
|
-
@param id number
|
|
176
|
-
@param eventId number
|
|
177
|
-
@param data Data
|
|
178
|
-
@param to string
|
|
179
|
-
@param sender string
|
|
180
|
-
@param fromServer string
|
|
181
|
-
@param invokedAt string
|
|
182
|
-
@param toServer string
|
|
183
|
-
@param gameId string
|
|
184
|
-
@param invokedBy number
|
|
185
|
-
@return Invocation
|
|
186
|
-
]]
|
|
187
|
-
function Invocation.new(id: number, eventId: number, data: Data, to: string, sender: string, fromServer: string, invokedAt: string, toServer: string, gameId: string, invokedBy: number)
|
|
188
|
-
local self = {}
|
|
189
|
-
self.id = id;
|
|
190
|
-
self.eventId = eventId;
|
|
191
|
-
self.data = data;
|
|
192
|
-
self.to = to;
|
|
193
|
-
self.sender = sender;
|
|
194
|
-
self.fromServer = fromServer;
|
|
195
|
-
self.invokedAt = invokedAt;
|
|
196
|
-
self.toServer = toServer;
|
|
197
|
-
self.gameId = gameId;
|
|
198
|
-
self.invokedBy = invokedBy;
|
|
199
|
-
|
|
200
|
-
return setmetatable(self, Invocation)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
--[[
|
|
204
|
-
|
|
205
|
-
______ _
|
|
206
|
-
| ____| | |
|
|
207
|
-
| |__ _____ _ __ | |_ ___
|
|
208
|
-
| __| / / _ \ '_ \| __/ __|
|
|
209
|
-
| |____ V / __/ | | | |_\__ \
|
|
210
|
-
|______\_/ \___|_| |_|\__|___/
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
This section defines the 'Event' object, which represents a configurable,
|
|
214
|
-
remote event that can be invoked from the game server or listened to.
|
|
215
|
-
]]
|
|
216
|
-
|
|
217
|
-
local Event = {}
|
|
218
|
-
Event.__index = Event
|
|
219
|
-
|
|
220
|
-
-- Represents a remote event defined in the Zyntex dashboard.
|
|
221
|
-
type EventType = {
|
|
222
|
-
id: number; -- The unique ID of the event.
|
|
223
|
-
name: string; -- The user-defined name of the event.
|
|
224
|
-
description: string; -- The user-defined description of the event.
|
|
225
|
-
dataStructure: Data; -- The schema defining the expected data payload.
|
|
226
|
-
deleted: boolean; -- Whether the event has been marked as deleted.
|
|
227
|
-
_zyntex: Zyntex; -- Internal reference to the main Zyntex client instance.
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export type Event = typeof(setmetatable({} :: EventType, Event))
|
|
231
|
-
|
|
232
|
-
--[[
|
|
233
|
-
@method Event:Invoke
|
|
234
|
-
@description Sends an invocation for this Event to the Zyntex backend.
|
|
235
|
-
This allows the game server to trigger events that can be logged or listened to by other systems.
|
|
236
|
-
The data structure provided must match the schema defined on the Zyntex dashboard.
|
|
237
|
-
|
|
238
|
-
@param self Event -- The event object to invoke.
|
|
239
|
-
@param data Data -- A table containing the key-value data payload for the event.
|
|
240
|
-
@return Invocation -- Returns an Invocation object if the API call is successful.
|
|
241
|
-
|
|
242
|
-
@see https://docs.zyntex.dev/ for more details on creating and using events.
|
|
243
|
-
|
|
244
|
-
@example
|
|
245
|
-
local killEvent = Zyntex:GetEvent("PlayerKilled")
|
|
246
|
-
killEvent:Invoke({
|
|
247
|
-
killerId = 12345,
|
|
248
|
-
victimId = 54321,
|
|
249
|
-
weapon = "Sword"
|
|
250
|
-
})
|
|
251
|
-
]]
|
|
252
|
-
function Event.Invoke(self: Event, data: Data): Invocation
|
|
253
|
-
-- Enqueue; response comes back via /push listen_data later.
|
|
254
|
-
self._zyntex._session:post(
|
|
255
|
-
`/roblox/events/invoke`,
|
|
256
|
-
{
|
|
257
|
-
["server_id"] = self._zyntex._session.jobID,
|
|
258
|
-
["event_id"] = self.id;
|
|
259
|
-
["data"] = data
|
|
260
|
-
}
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
-- Return a synthetic "queued" invocation placeholder
|
|
264
|
-
return Invocation.new(
|
|
265
|
-
-1,
|
|
266
|
-
self.id,
|
|
267
|
-
data,
|
|
268
|
-
"web",
|
|
269
|
-
"roblox",
|
|
270
|
-
self._zyntex._session.jobID,
|
|
271
|
-
now(),
|
|
272
|
-
nil,
|
|
273
|
-
0,
|
|
274
|
-
0
|
|
275
|
-
)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
--[[
|
|
279
|
-
@method Event:Connect
|
|
280
|
-
@description Establishes a listener for this specific event. The provided callback function
|
|
281
|
-
will be executed whenever an invocation for this event is received from the Zyntex backend (e.g., sent from the dashboard).
|
|
282
|
-
|
|
283
|
-
@param self Event -- The event object to listen to.
|
|
284
|
-
@param listener (({[string]: any}, Invocation) -> nil) -- The callback function to execute.
|
|
285
|
-
- The first argument passed to the listener is a simplified data table: `{["key"] = value}`.
|
|
286
|
-
- The second argument is the full, raw Invocation object.
|
|
287
|
-
|
|
288
|
-
@example
|
|
289
|
-
local broadcastEvent = Zyntex:GetEvent("BroadcastMessage")
|
|
290
|
-
broadcastEvent:Connect(function(data, invocation)
|
|
291
|
-
print(`Received broadcast from user {invocation.invokedBy}: {data.Message}`)
|
|
292
|
-
end)
|
|
293
|
-
]]
|
|
294
|
-
function Event.Connect(self: Event, listener: ({[string]: any}, Invocation) -> nil)
|
|
295
|
-
local function wrapper(inv)
|
|
296
|
-
if inv.event_id == self.id then
|
|
297
|
-
local simpleData = {}
|
|
298
|
-
|
|
299
|
-
for _,v in pairs(inv.data) do
|
|
300
|
-
simpleData[v["key"]] = v["value"]
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
listener(
|
|
304
|
-
simpleData,
|
|
305
|
-
Invocation.new(
|
|
306
|
-
inv.id,
|
|
307
|
-
inv.event_id,
|
|
308
|
-
inv.data,
|
|
309
|
-
inv.to,
|
|
310
|
-
inv.sender,
|
|
311
|
-
inv.from_server,
|
|
312
|
-
inv.invoked_at,
|
|
313
|
-
inv.to_server,
|
|
314
|
-
inv.game_id,
|
|
315
|
-
inv.invoked_by
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
if not self._zyntex._listeners["invocation"] then
|
|
321
|
-
self._zyntex._listeners["invocation"] = {}
|
|
322
|
-
end
|
|
323
|
-
table.insert(
|
|
324
|
-
self._zyntex._listeners["invocation"],
|
|
325
|
-
wrapper
|
|
326
|
-
)
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
--[[
|
|
330
|
-
@constructor Event.new
|
|
331
|
-
@description Constructs a new Event object. This is used internally during the initialization
|
|
332
|
-
process to populate the list of available events from the Zyntex backend.
|
|
333
|
-
]]
|
|
334
|
-
function Event.new(zyntex: Zyntex, id: number, name: string, description: string, dataStructure: DataSchema, deleted: boolean)
|
|
335
|
-
local self = {}
|
|
336
|
-
self.id = id
|
|
337
|
-
self.name = name
|
|
338
|
-
self.description = description
|
|
339
|
-
self.dataStructure = dataStructure
|
|
340
|
-
self.deleted = deleted
|
|
341
|
-
self._zyntex = zyntex
|
|
342
|
-
|
|
343
|
-
return setmetatable(self, Event)
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
--[[
|
|
347
|
-
______ _ _
|
|
348
|
-
| ____| | | (_)
|
|
349
|
-
| |__ _ _ _ __ ___| |_ _ ___ _ __ ___
|
|
350
|
-
| __| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
|
|
351
|
-
| | | |_| | | | | (__| |_| | (_) | | | \__ \
|
|
352
|
-
|_| \__,_|_| |_|\___|\__|_|\___/|_| |_|___/
|
|
353
|
-
|
|
354
|
-
This section defines the 'Function' object, which is similar to events, but
|
|
355
|
-
can only be invoked from the web-dashboard and it sends back data
|
|
356
|
-
]]
|
|
357
|
-
|
|
358
|
-
local Function = {}
|
|
359
|
-
Function.__index = Function
|
|
360
|
-
|
|
361
|
-
type FunctionParameter = {
|
|
362
|
-
name: string; -- The name of the paramater
|
|
363
|
-
type: string; -- The type of the parameter's value
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
-- Represents a remote function defined in the Zyntex dashboard.
|
|
367
|
-
type FunctionType = {
|
|
368
|
-
id: number; -- The unique ID of the function.
|
|
369
|
-
name: string; -- The user-defined name of the event.
|
|
370
|
-
description: string; -- The user-defined description of the event.
|
|
371
|
-
parameters: {FunctionParameter}; -- The list of parameters the function accepts
|
|
372
|
-
_zyntex: Zyntex;
|
|
373
|
-
|
|
374
|
-
Connect: (self: Function, listener: ({[string]: any}) -> any) -> nil;
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export type Function = typeof(setmetatable({} :: FunctionType, Function))
|
|
379
|
-
|
|
380
|
-
--[[
|
|
381
|
-
@constructor Function.new
|
|
382
|
-
@description Constructs a new Function object that represents a callable remote function
|
|
383
|
-
defined in the Zyntex dashboard. This is typically used internally when the client
|
|
384
|
-
initializes and materializes the functions available to your experience.
|
|
385
|
-
|
|
386
|
-
@param zyntex Zyntex -- The active Zyntex client, used for networking and configuration.
|
|
387
|
-
@param id number -- The unique ID of the remote function.
|
|
388
|
-
@param name string -- The user-defined name of the function.
|
|
389
|
-
@param description string -- A human-readable description of the function.
|
|
390
|
-
@param parameters {FunctionParameter} -- The schema describing the parameters this function accepts.
|
|
391
|
-
|
|
392
|
-
@return Function -- The constructed Function instance.
|
|
393
|
-
]]
|
|
394
|
-
function Function.new(zyntex: Zyntex, id: number, name: string, description: string, parameters: {FunctionParameter})
|
|
395
|
-
local self = {}
|
|
396
|
-
self.id = id
|
|
397
|
-
self.name = name
|
|
398
|
-
self.description = description
|
|
399
|
-
self.parameters = parameters
|
|
400
|
-
self._zyntex = zyntex
|
|
401
|
-
|
|
402
|
-
return setmetatable(self, Function)
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
--[[
|
|
406
|
-
@method Function:Connect
|
|
407
|
-
@description Registers a listener that will be invoked whenever a call targeting this
|
|
408
|
-
remote function is received from the Zyntex backend. The listener is passed a simple
|
|
409
|
-
parameters table and is expected to return a value (typically a table) which will be
|
|
410
|
-
sent back to Zyntex as the payload response.
|
|
411
|
-
|
|
412
|
-
@param self Function -- The Function instance to listen on.
|
|
413
|
-
@param listener (({[string]: any}) -> any) -- Callback executed when this function is invoked.
|
|
414
|
-
- Receives: a table of parameter key/value pairs (`metadata.parameters`).
|
|
415
|
-
- Returns: any serializable value (commonly a table) that becomes the payload response.
|
|
416
|
-
|
|
417
|
-
@return nil -- This method registers the listener and does not return a connection handle.
|
|
418
|
-
|
|
419
|
-
@errors Asserts if the payload POST fails; the assertion message is the API's `user_message`.
|
|
420
|
-
|
|
421
|
-
@example
|
|
422
|
-
-- Assume a remote function "Add" with parameters: { a: number, b: number }
|
|
423
|
-
local addFn = Zyntex:GetFunction("Add")
|
|
424
|
-
addFn:Connect(function(params)
|
|
425
|
-
local a = params.a
|
|
426
|
-
local b = params.b
|
|
427
|
-
-- Whatever is returned here is sent back to Zyntex as the payload:
|
|
428
|
-
return { sum = a + b }
|
|
429
|
-
end)
|
|
430
|
-
]]
|
|
431
|
-
function Function.Connect(self: Function, listener: ({[string]: any}) -> any)
|
|
432
|
-
local function wrapper(data)
|
|
433
|
-
if data.metadata.function_id == self.id then
|
|
434
|
-
if self._zyntex._config.debug then
|
|
435
|
-
print(`[Zyntex]: Function received, calling listener...`)
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
local payload = listener(
|
|
439
|
-
data.metadata.parameters
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
if self._zyntex._config.debug then
|
|
443
|
-
print(`[Zyntex]: Sending payload response...`)
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
assert(payload ~= nil, 'Function:Connect hook must return a value')
|
|
447
|
-
|
|
448
|
-
local callRes = self._zyntex._session:post(
|
|
449
|
-
`/roblox/actions/{data.id}/send_payload`,
|
|
450
|
-
{ ["data"] = payload },
|
|
451
|
-
true
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
if self._zyntex._config.debug then
|
|
455
|
-
print(`[Zyntex]: Successfully sent payload response to function`)
|
|
456
|
-
end
|
|
457
|
-
end
|
|
458
|
-
end
|
|
459
|
-
if not self._zyntex._listeners["function"] then
|
|
460
|
-
self._zyntex._listeners["function"] = {}
|
|
461
|
-
end
|
|
462
|
-
table.insert(
|
|
463
|
-
self._zyntex._listeners["function"],
|
|
464
|
-
wrapper
|
|
465
|
-
)
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
--[[
|
|
470
|
-
|
|
471
|
-
______ _
|
|
472
|
-
|___ / | |
|
|
473
|
-
/ /_ __ _ _ __ | |_ _____ __
|
|
474
|
-
/ /| | | | '_ \| __/ _ \ \/ /
|
|
475
|
-
/ /_| |_| | | | | || __/> <
|
|
476
|
-
/_____\__, |_| |_|\__\___/_/\_\
|
|
477
|
-
__/ |
|
|
478
|
-
|___/
|
|
479
|
-
|
|
480
|
-
This section defines the main 'Zyntex' client object, which serves as the primary
|
|
481
|
-
interface for interacting with the entire Zyntex system.
|
|
482
|
-
]]
|
|
483
|
-
|
|
484
|
-
local Zyntex = { VERSION = CLIENT_VERSION }
|
|
485
|
-
Zyntex.__index = Zyntex
|
|
486
|
-
|
|
487
|
-
-- The main state container for the Zyntex client.
|
|
488
|
-
type ZyntexType = {
|
|
489
|
-
_session: API.Session; -- Handles authenticated requests to the Zyntex API.
|
|
490
|
-
_config: TYPES.Config; -- Stores the user-defined configuration for the client.
|
|
491
|
-
_events: {Event}; -- A list of all available Event objects fetched from the backend.
|
|
492
|
-
_functions: {Function}; -- A list of all available Function objects fetched from the backend.
|
|
493
|
-
_listeners: {[string]: ({[string]: any}) -> nil}; -- A dictionary of listeners for incoming actions (e.g., 'shutdown', 'rce').
|
|
494
|
-
_version: number;
|
|
495
|
-
_pendingJoins: {[string]: Player}; -- key: batch rid -> Player
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export type Zyntex = typeof(setmetatable({} :: ZyntexType, Zyntex))
|
|
499
|
-
|
|
500
|
-
local maxPlayers = 0;
|
|
501
|
-
|
|
502
|
-
--[[
|
|
503
|
-
@function serverStatus
|
|
504
|
-
@description Gathers real-time performance metrics about the current server instance.
|
|
505
|
-
@return table -- A dictionary containing health status, memory usage, network traffic, and FPS.
|
|
506
|
-
]]
|
|
507
|
-
local function serverStatus()
|
|
508
|
-
local maxMemory = 6400 + 100 * maxPlayers
|
|
509
|
-
local stats = {
|
|
510
|
-
["memory_usage"] = Stats:GetTotalMemoryUsageMb() / maxMemory,
|
|
511
|
-
["data_receive_kbps"] = Stats.DataReceiveKbps or 0,
|
|
512
|
-
["data_send_kbps"] = Stats.DataSendKbps or 0,
|
|
513
|
-
["server_fps"] = math.clamp(1/RunService.Heartbeat:Wait(), 0, 100),
|
|
514
|
-
["activity"] = SERVER_ACTIVITY
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
if stats.memory_usage > .5 or stats.data_send_kbps > 500 or stats.data_receive_kbps > 500 or stats.server_fps < 30 then
|
|
518
|
-
stats["health"] = "unhealthy"
|
|
519
|
-
return stats
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
stats["health"] = "healthy"
|
|
523
|
-
return stats
|
|
524
|
-
end
|
|
525
|
-
|
|
526
|
-
--[[
|
|
527
|
-
@method Zyntex:statusUpdate
|
|
528
|
-
@description Sends the server's current health and performance metrics to the Zyntex dashboard.
|
|
529
|
-
@param self Zyntex
|
|
530
|
-
@param status {string: number} -- A table of metrics generated by `serverStatus()`.
|
|
531
|
-
]]
|
|
532
|
-
function Zyntex.statusUpdate(self: Zyntex, status: {string: number})
|
|
533
|
-
if sendingStatus then return end
|
|
534
|
-
sendingStatus = true
|
|
535
|
-
if self._config.debug then
|
|
536
|
-
print("[Zyntex]: Submitting server status update...")
|
|
537
|
-
end
|
|
538
|
-
|
|
539
|
-
pcall(function()
|
|
540
|
-
self._session:post(
|
|
541
|
-
"/roblox/servers/status",
|
|
542
|
-
table.freeze({
|
|
543
|
-
["server_id"] = self._session.jobID,
|
|
544
|
-
["memory_usage"] = status["memory_usage"],
|
|
545
|
-
["server_fps"] = status["server_fps"],
|
|
546
|
-
["data_send_kbps"] = status["data_send_kbps"],
|
|
547
|
-
["data_receive_kbps"] = status["data_receive_kbps"],
|
|
548
|
-
["activity"] = status["activity"],
|
|
549
|
-
["health"] = if status["health"] == "unhealthy" then "unhealthy" else "healthy"
|
|
550
|
-
})
|
|
551
|
-
)
|
|
552
|
-
if self._config.debug then
|
|
553
|
-
print("[Zyntex]: Sent server status update (queued)")
|
|
554
|
-
end
|
|
555
|
-
end)
|
|
556
|
-
|
|
557
|
-
sendingStatus = false
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
--[[
|
|
561
|
-
@function statusUpdateLoop
|
|
562
|
-
@description A background loop that periodically collects and sends server status updates.
|
|
563
|
-
It intelligently sends updates only when metrics have changed to reduce unnecessary network traffic.
|
|
564
|
-
@param self Zyntex
|
|
565
|
-
]]
|
|
566
|
-
local function statusUpdateLoop(self: Zyntex)
|
|
567
|
-
local lastStatus = serverStatus()
|
|
568
|
-
|
|
569
|
-
while true do
|
|
570
|
-
task.wait(15)
|
|
571
|
-
|
|
572
|
-
local newStatus = serverStatus()
|
|
573
|
-
|
|
574
|
-
self:statusUpdate(newStatus)
|
|
575
|
-
end
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
--[[
|
|
579
|
-
@function onPlayerAdd
|
|
580
|
-
@description Handles all tasks associated with a player joining the game, including
|
|
581
|
-
registering them with the Zyntex backend and setting up client-side scripts.
|
|
582
|
-
@param self Zyntex
|
|
583
|
-
@param player Player -- The player who just joined.
|
|
584
|
-
]]
|
|
585
|
-
local function onPlayerAdd(self: Zyntex, player: Player)
|
|
586
|
-
if self._config.debug then
|
|
587
|
-
print(`[Zyntex]: Handling player join for {player.Name}`)
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
local when = now()
|
|
591
|
-
local activity = {
|
|
592
|
-
player_id = player.UserId,
|
|
593
|
-
player_name = player.Name,
|
|
594
|
-
joined_at = when
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
-- Enqueue join; **capture the batch item's rid** returned by api.post
|
|
598
|
-
local rid = self._session:post(
|
|
599
|
-
"/roblox/players",
|
|
600
|
-
{
|
|
601
|
-
["server_id"] = self._session.jobID,
|
|
602
|
-
["id"] = player.UserId,
|
|
603
|
-
["name"] = player.Name,
|
|
604
|
-
["timestamp"] = when
|
|
605
|
-
}
|
|
606
|
-
)
|
|
607
|
-
-- Track pending join by the **batch rid** so we can reconcile on push results
|
|
608
|
-
self._pendingJoins[rid] = player
|
|
609
|
-
|
|
610
|
-
pcall(function()
|
|
611
|
-
-- No immediate response under /push; moderation blocks are surfaced via server on next poll.
|
|
612
|
-
|
|
613
|
-
if self._config.debug then
|
|
614
|
-
print("[Zyntex]: Player join submitted.")
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
local zyntexEvents = Instance.new("Folder")
|
|
618
|
-
zyntexEvents.Name = "zyntex.events"
|
|
619
|
-
zyntexEvents.Parent = script.Parent
|
|
620
|
-
|
|
621
|
-
local SystemChat = Instance.new("RemoteEvent")
|
|
622
|
-
SystemChat.Name = "SystemChat"
|
|
623
|
-
SystemChat.Parent = zyntexEvents
|
|
624
|
-
|
|
625
|
-
-- Clone necessary client-side remote events into ReplicatedStorage if they don't exist.
|
|
626
|
-
if not game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events") then
|
|
627
|
-
zyntexEvents:Clone().Parent = game:GetService("ReplicatedStorage")
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
-- Provide the player with the client-side script handler.
|
|
631
|
-
script.Parent:FindFirstChild("zyntex.client"):Clone().Parent = player.PlayerGui
|
|
632
|
-
|
|
633
|
-
-- Listen for player chats and log them.
|
|
634
|
-
pcall(function()
|
|
635
|
-
player.Chatted:Connect(function(msg)
|
|
636
|
-
self._session:post(
|
|
637
|
-
"/roblox/players/chat",
|
|
638
|
-
{
|
|
639
|
-
["server_id"] = self._session.jobID,
|
|
640
|
-
["message"] = msg;
|
|
641
|
-
["player_id"] = player.UserId
|
|
642
|
-
}
|
|
643
|
-
)
|
|
644
|
-
end)
|
|
645
|
-
end)
|
|
646
|
-
|
|
647
|
-
-- Track the maximum concurrent players for server health calculations.
|
|
648
|
-
local count = #Players:GetPlayers()
|
|
649
|
-
if count > maxPlayers then
|
|
650
|
-
maxPlayers = count
|
|
651
|
-
end
|
|
652
|
-
end)
|
|
653
|
-
|
|
654
|
-
-- visit_id will be reconciled on the backend; we don't have it synchronously here.
|
|
655
|
-
|
|
656
|
-
table.insert(SERVER_ACTIVITY, activity)
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
--[[
|
|
660
|
-
@function onPlayerRemove
|
|
661
|
-
@description Handles a player leaving the game by notifying the Zyntex backend,
|
|
662
|
-
which marks the player's session as ended.
|
|
663
|
-
@param self Zyntex
|
|
664
|
-
@param player Player -- The player who just left.
|
|
665
|
-
]]
|
|
666
|
-
local function onPlayerRemove(self: Zyntex, player: Player)
|
|
667
|
-
if self._config.debug then
|
|
668
|
-
print(`[Zyntex]: Handling player leave for {player.Name}`)
|
|
669
|
-
end
|
|
670
|
-
|
|
671
|
-
local when = now()
|
|
672
|
-
|
|
673
|
-
-- Clean up any pending joins for this player
|
|
674
|
-
for rid, pendingPlayer in pairs(self._pendingJoins) do
|
|
675
|
-
if pendingPlayer.UserId == player.UserId then
|
|
676
|
-
self._pendingJoins[rid] = nil
|
|
677
|
-
if self._config.debug then
|
|
678
|
-
print(("[Zyntex]: Cleaned up pending join for leaving player %s (RID: %s)"):format(player.Name, rid))
|
|
679
|
-
end
|
|
680
|
-
break
|
|
681
|
-
end
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
-- Enqueue leave (handled by /roblox/push). Fire-and-forget.
|
|
685
|
-
self._session:delete(
|
|
686
|
-
"/roblox/players",
|
|
687
|
-
{
|
|
688
|
-
["server_id"] = self._session.jobID,
|
|
689
|
-
["id"] = player.UserId,
|
|
690
|
-
["timestamp"] = when
|
|
691
|
-
}
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
if self._config.debug then
|
|
695
|
-
print("[Zyntex]: Player leave submitted.")
|
|
696
|
-
end
|
|
697
|
-
|
|
698
|
-
for _,activity in SERVER_ACTIVITY do
|
|
699
|
-
if activity.player_id == player.UserId and activity.left_at == nil then
|
|
700
|
-
activity["left_at"] = when
|
|
701
|
-
end
|
|
702
|
-
end
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
--[[
|
|
706
|
-
@method Zyntex:GetEventByID
|
|
707
|
-
@description Fetches a pre-loaded Event object using its unique numerical ID.
|
|
708
|
-
@param self Zyntex
|
|
709
|
-
@param eventId number -- The ID of the event to retrieve.
|
|
710
|
-
@return Event? -- The corresponding Event object, if exists.
|
|
711
|
-
]]
|
|
712
|
-
function Zyntex.GetEventByID(self: Zyntex, eventId: number): Event?
|
|
713
|
-
for i,event in pairs(self._events) do
|
|
714
|
-
if event.id == eventId then
|
|
715
|
-
return event
|
|
716
|
-
end
|
|
717
|
-
end
|
|
718
|
-
return nil
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
--[[
|
|
722
|
-
@method Zyntex:GetEvent
|
|
723
|
-
@description Fetches a pre-loaded Event object using its unique string name. This is the most common way to get an event.
|
|
724
|
-
@param self Zyntex
|
|
725
|
-
@param eventName string -- The case-sensitive name of the event.
|
|
726
|
-
@return Event? -- The corresponding Event object, if it exists.
|
|
727
|
-
]]
|
|
728
|
-
function Zyntex.GetEvent(self: Zyntex, eventName: string): Event?
|
|
729
|
-
for i,event in pairs(self._events) do
|
|
730
|
-
if event.name == eventName then
|
|
731
|
-
return event
|
|
732
|
-
end
|
|
733
|
-
end
|
|
734
|
-
return nil
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
--[[
|
|
738
|
-
@method Zyntex:GetFunctionByID
|
|
739
|
-
@description Fetches a pre-loaded Function object using its unique numerical ID.
|
|
740
|
-
@param self Zyntex
|
|
741
|
-
@param functionId number -- The ID of the function to retrieve.
|
|
742
|
-
@return Function? -- The corresponding Function object, if it exists.
|
|
743
|
-
]]
|
|
744
|
-
function Zyntex.GetFunctionByID(self: Zyntex, functionId: number): Function?
|
|
745
|
-
for i,func in self._functions do
|
|
746
|
-
if func.id == functionId then
|
|
747
|
-
return func
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
return nil
|
|
751
|
-
end
|
|
752
|
-
|
|
753
|
-
--[[
|
|
754
|
-
@method Zyntex:GetFunction
|
|
755
|
-
@description Fetches a pre-loaded Function object using its unique string name. This is the most common way to get a function.
|
|
756
|
-
@param self Zyntex
|
|
757
|
-
@param functionName string -- The case-sensitive name of the function.
|
|
758
|
-
@return Function? -- The corresponding Function object, if it exists.
|
|
759
|
-
]]
|
|
760
|
-
function Zyntex.GetFunction(self: Zyntex, functionName: string): Function?
|
|
761
|
-
for i,func in self._functions do
|
|
762
|
-
if func.name == functionName then
|
|
763
|
-
return func
|
|
764
|
-
end
|
|
765
|
-
end
|
|
766
|
-
return nil
|
|
767
|
-
end
|
|
768
|
-
|
|
769
|
-
--[[
|
|
770
|
-
@method Zyntex:Moderate
|
|
771
|
-
@description Submits a generic moderation action to the Zyntex backend. This is a flexible, low-level function;
|
|
772
|
-
it is often easier to use the shorthand methods (:Ban, :Kick, :Mute, :Report).
|
|
773
|
-
|
|
774
|
-
@warning If the `test` parameter is `false` or `nil` (and not in Studio), this action WILL permanently affect a player's reputation.
|
|
775
|
-
|
|
776
|
-
@param self Zyntex
|
|
777
|
-
@param player (Player | number) -- The Player object or the UserId of the target.
|
|
778
|
-
@param type string -- The type of moderation (e.g., "ban", "kick", "mute", "report").
|
|
779
|
-
@param reason string -- The reason for the moderation action. This will be visible to staff.
|
|
780
|
-
@param expiresAt (DateTime?) -- An optional DateTime for when the moderation expires. If nil, it may be permanent depending on the type.
|
|
781
|
-
@param test (boolean?) -- If `true`, the moderation is treated as a test and does not affect reputation. Defaults to `true` in Studio.
|
|
782
|
-
@return boolean -- Returns `true` on success.
|
|
783
|
-
]]
|
|
784
|
-
function Zyntex.Moderate(self: Zyntex, player: Player | number, type: string, reason: string, expiresAt: DateTime?, test: boolean?)
|
|
785
|
-
local playerId = if typeof(player) == "Player" then player.UserId else player
|
|
786
|
-
local playerObject = Players:GetPlayerByUserId(playerId)
|
|
787
|
-
if self._config.debug then
|
|
788
|
-
print(`[Zyntex]: Creating moderation {type} for {playerId}...`)
|
|
789
|
-
end
|
|
790
|
-
if expiresAt then
|
|
791
|
-
expiresAt = toFormat(expiresAt)
|
|
792
|
-
end
|
|
793
|
-
|
|
794
|
-
-- Default to test mode if running in Roblox Studio unless explicitly overridden.
|
|
795
|
-
if test == nil then
|
|
796
|
-
test = RunService:IsStudio()
|
|
797
|
-
end
|
|
798
|
-
|
|
799
|
-
if type == "ban" then
|
|
800
|
-
playerObject:Kick(reason)
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
local res = self._session:post(
|
|
804
|
-
`/roblox/players/{playerId}/moderate`,
|
|
805
|
-
{
|
|
806
|
-
["server_id"] = self._session.jobID,
|
|
807
|
-
["type"] = type,
|
|
808
|
-
["reason"] = reason,
|
|
809
|
-
["expires_at"] = expiresAt,
|
|
810
|
-
["test"] = test
|
|
811
|
-
}
|
|
812
|
-
)
|
|
813
|
-
|
|
814
|
-
if self._config.debug then
|
|
815
|
-
print(`[Zyntex]: Moderation created successfully.`)
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
return true
|
|
819
|
-
end
|
|
820
|
-
|
|
821
|
-
--[[
|
|
822
|
-
@method Zyntex:Report
|
|
823
|
-
@description Shorthand method to create a "report" moderation. Reports are used for logging player behavior
|
|
824
|
-
and contribute to their reputation score but do not have direct in-game consequences by default.
|
|
825
|
-
@param self Zyntex
|
|
826
|
-
@param player (Player | number) -- The Player object or the UserId of the target.
|
|
827
|
-
@param reason string -- The reason for the report.
|
|
828
|
-
@param test (boolean?) -- If true, the report will not affect player reputation. Defaults to `true` in Studio.
|
|
829
|
-
@return boolean -- Returns `true` on success.
|
|
830
|
-
]]
|
|
831
|
-
function Zyntex.Report(self: Zyntex, player: Player | number, reason: string, test: boolean?)
|
|
832
|
-
return self:Moderate(player, "report", reason, nil, test)
|
|
833
|
-
end
|
|
834
|
-
|
|
835
|
-
--[[
|
|
836
|
-
@method Zyntex:Ban
|
|
837
|
-
@description Shorthand method to create a "ban" moderation. This logs the ban action.
|
|
838
|
-
Handles automatic kick automatically.
|
|
839
|
-
@param self Zyntex
|
|
840
|
-
@param player (Player | number) -- The Player object or the UserId of the target.
|
|
841
|
-
@param reason string -- The reason for the ban.
|
|
842
|
-
@param expiresAt (DateTime?) -- Optional expiration for the ban. If nil, the ban is permanent.
|
|
843
|
-
@param test (boolean?) -- If true, the ban will not affect player reputation. Defaults to `true` in Studio.
|
|
844
|
-
@return boolean -- Returns `true` on success.
|
|
845
|
-
]]
|
|
846
|
-
function Zyntex.Ban(self: Zyntex, player: Player | number, reason: string, expiresAt: DateTime?, test: boolean?)
|
|
847
|
-
return self:Moderate(player, "ban", reason, expiresAt, test)
|
|
848
|
-
end
|
|
849
|
-
|
|
850
|
-
--[[
|
|
851
|
-
@method Zyntex:Mute
|
|
852
|
-
@description Shorthand method to create a "mute" moderation. This logs the mute action.
|
|
853
|
-
Mutes the player automatically.
|
|
854
|
-
@param self Zyntex
|
|
855
|
-
@param player (Player | number) -- The Player object or the UserId of the target.
|
|
856
|
-
@param reason string -- The reason for the mute.
|
|
857
|
-
@param expiresAt (DateTime?) -- Optional expiration for the mute. If nil, the mute is permanent.
|
|
858
|
-
@param test (boolean?) -- If true, the mute will not affect player reputation. Defaults to `true` in Studio.
|
|
859
|
-
@return boolean -- Returns `true` on success.
|
|
860
|
-
]]
|
|
861
|
-
function Zyntex.Mute(self: Zyntex, player: Player | number, reason: string, expiresAt: DateTime?, test: boolean?)
|
|
862
|
-
return self:Moderate(player, "mute", reason, expiresAt, test)
|
|
863
|
-
end
|
|
864
|
-
|
|
865
|
-
--[[
|
|
866
|
-
@method Zyntex:Kick
|
|
867
|
-
@description Shorthand method to create a "kick" moderation. This logs the kick action.
|
|
868
|
-
Calls Player:Kick() automatically.
|
|
869
|
-
@param self Zyntex
|
|
870
|
-
@param player (Player | number) -- The Player object or the UserId of the target.
|
|
871
|
-
@param reason string -- The reason for the kick.
|
|
872
|
-
@param test (boolean?) -- If true, the kick will not affect player reputation. Defaults to `true` in Studio.
|
|
873
|
-
@return boolean -- Returns `true` on success.
|
|
874
|
-
]]
|
|
875
|
-
function Zyntex.Kick(self: Zyntex, player: Player | number, reason: string, test: boolean?)
|
|
876
|
-
return self:Moderate(player, "kick", reason, nil, test)
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
--[[
|
|
882
|
-
@method Zyntex:CleanupPendingJoins
|
|
883
|
-
@description Manually clean up pending joins that may have been stuck.
|
|
884
|
-
This is useful for debugging or in case of network issues.
|
|
885
|
-
@param self Zyntex
|
|
886
|
-
@return number -- Returns the number of pending joins that were cleaned up.
|
|
887
|
-
]]
|
|
888
|
-
-- Note: CleanupPendingJoins removed - joins are cleaned up immediately on result
|
|
889
|
-
|
|
890
|
-
--[[
|
|
891
|
-
@method Zyntex:GetPendingJoinsCount
|
|
892
|
-
@description Get the current count of pending joins.
|
|
893
|
-
This is useful for monitoring the state of join operations.
|
|
894
|
-
@param self Zyntex
|
|
895
|
-
@return number -- Returns the number of pending joins.
|
|
896
|
-
]]
|
|
897
|
-
function Zyntex.GetPendingJoinsCount(self: Zyntex)
|
|
898
|
-
local count = 0
|
|
899
|
-
for _ in pairs(self._pendingJoins) do
|
|
900
|
-
count = count + 1
|
|
901
|
-
end
|
|
902
|
-
return count
|
|
903
|
-
end
|
|
904
|
-
|
|
905
|
-
--[[
|
|
906
|
-
@method Zyntex:poll
|
|
907
|
-
@description Initiates the long-polling loop to listen for real-time actions and invocations
|
|
908
|
-
from the Zyntex dashboard. This runs in its own thread.
|
|
909
|
-
@param self Zyntex
|
|
910
|
-
@param since string -- An ISO 8601 timestamp to start listening from.
|
|
911
|
-
@return (-> ()) -- Returns the polling function to be spawned in a new thread.
|
|
912
|
-
]]
|
|
913
|
-
function Zyntex.poll(self: Zyntex, since: string)
|
|
914
|
-
return function()
|
|
915
|
-
local nextWaitTime = Random.new():NextInteger(4, 7)
|
|
916
|
-
while true do
|
|
917
|
-
if self._config.debug then
|
|
918
|
-
print("[Zyntex]: Flushing /push buffer (poll)...")
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
local items = {}
|
|
922
|
-
local ok, resp = pcall(function()
|
|
923
|
-
return self._session:Flush(since)
|
|
924
|
-
end)
|
|
925
|
-
if ok and resp and resp.success then
|
|
926
|
-
items = (resp.data and resp.data.listen_data) or {}
|
|
927
|
-
if #items == 0 then
|
|
928
|
-
nextWaitTime = math.clamp(nextWaitTime + 0.1, 2, 10)
|
|
929
|
-
else
|
|
930
|
-
nextWaitTime = 2
|
|
931
|
-
since = now()
|
|
932
|
-
end
|
|
933
|
-
else
|
|
934
|
-
nextWaitTime = nextWaitTime + 10
|
|
935
|
-
end
|
|
936
|
-
|
|
937
|
-
-- Dispatch any received events
|
|
938
|
-
for _, entry in pairs(items) do
|
|
939
|
-
local bucket = self._listeners[entry.type]
|
|
940
|
-
if bucket then
|
|
941
|
-
for _, listener in pairs(bucket) do
|
|
942
|
-
listener(entry.data)
|
|
943
|
-
end
|
|
944
|
-
end
|
|
945
|
-
end
|
|
946
|
-
|
|
947
|
-
-- React to per-item results: moderate on failed joins (401/403)
|
|
948
|
-
if resp and resp.data and resp.data.results then
|
|
949
|
-
for _, result in pairs(resp.data.results) do
|
|
950
|
-
-- We only care about failed player joins here
|
|
951
|
-
if result.method == "POST /roblox/players" and result.rid then
|
|
952
|
-
local rid = result.rid
|
|
953
|
-
local player = self._pendingJoins[rid]
|
|
954
|
-
if player then
|
|
955
|
-
if result.success == true then
|
|
956
|
-
-- successfully admitted; clear pending
|
|
957
|
-
self._pendingJoins[rid] = nil
|
|
958
|
-
if self._config.debug then
|
|
959
|
-
print(`[Zyntex]: Join confirmed for {player.Name} (RID: {rid})`)
|
|
960
|
-
end
|
|
961
|
-
else
|
|
962
|
-
-- Parse "CODE: reason" -> e.g. "403: racism" / "401: spam"
|
|
963
|
-
local codeStr, reason = nil, nil
|
|
964
|
-
if typeof(result.error) == "string" then
|
|
965
|
-
codeStr, reason = string.match(result.error, "^(%d+)%s*:%s*(.+)$")
|
|
966
|
-
end
|
|
967
|
-
local code = tonumber(codeStr or "")
|
|
968
|
-
reason = reason or "Moderation"
|
|
969
|
-
|
|
970
|
-
if code == 403 then
|
|
971
|
-
-- BAN: kick immediately + fire ban listeners
|
|
972
|
-
local p = Players:GetPlayerByUserId(player.UserId)
|
|
973
|
-
if p then p:Kick(reason) end
|
|
974
|
-
if self._listeners["ban"] then
|
|
975
|
-
for _,cb in self._listeners["ban"] do
|
|
976
|
-
cb(player.UserId, reason)
|
|
977
|
-
end
|
|
978
|
-
end
|
|
979
|
-
if self._config.debug then
|
|
980
|
-
warn(`[Zyntex]: {player.Name} blocked (BAN) — {reason}`)
|
|
981
|
-
end
|
|
982
|
-
elseif code == 401 then
|
|
983
|
-
-- MUTE: silence chat + fire mute listeners
|
|
984
|
-
muteUserId(player.UserId)
|
|
985
|
-
if self._listeners["mute"] then
|
|
986
|
-
for _,cb in self._listeners["mute"] do
|
|
987
|
-
cb(player.UserId, reason)
|
|
988
|
-
end
|
|
989
|
-
end
|
|
990
|
-
if self._config.debug then
|
|
991
|
-
warn(`[Zyntex]: {player.Name} auto-muted — {reason}`)
|
|
992
|
-
end
|
|
993
|
-
else
|
|
994
|
-
-- Unknown failure; just warn
|
|
995
|
-
warn(`[Zyntex]: Join failed for {player.Name} (RID: {rid}) — {tostring(result.error)}`)
|
|
996
|
-
end
|
|
997
|
-
|
|
998
|
-
-- In all error cases, clear pending
|
|
999
|
-
self._pendingJoins[rid] = nil
|
|
1000
|
-
end
|
|
1001
|
-
elseif self._config.debug then
|
|
1002
|
-
print(`[Zyntex]: Result for unknown RID (possibly already cleaned): {result.rid}`)
|
|
1003
|
-
end
|
|
1004
|
-
end
|
|
1005
|
-
end
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
task.wait(nextWaitTime)
|
|
1009
|
-
end
|
|
1010
|
-
end
|
|
1011
|
-
end
|
|
1012
|
-
|
|
1013
|
-
--[[
|
|
1014
|
-
@method Zyntex:logServiceMessage
|
|
1015
|
-
@description Internal handler for capturing messages from Roblox's LogService (print, warn, error)
|
|
1016
|
-
and forwarding them to the Zyntex logs for the server.
|
|
1017
|
-
@param self Zyntex
|
|
1018
|
-
@param msg string -- The content of the log message.
|
|
1019
|
-
@param type Enum.MessageType -- The type of message (Output, Info, Warning, Error).
|
|
1020
|
-
]]
|
|
1021
|
-
function Zyntex.logServiceMessage(self: Zyntex, msg: string, type: Enum.MessageType)
|
|
1022
|
-
local convertedType: string?
|
|
1023
|
-
if type == Enum.MessageType.MessageOutput then
|
|
1024
|
-
convertedType = "info.print"
|
|
1025
|
-
end
|
|
1026
|
-
if type == Enum.MessageType.MessageError then
|
|
1027
|
-
convertedType = "info.error"
|
|
1028
|
-
end
|
|
1029
|
-
if type == Enum.MessageType.MessageWarning then
|
|
1030
|
-
convertedType = "info.warning"
|
|
1031
|
-
end
|
|
1032
|
-
if convertedType == nil then
|
|
1033
|
-
return type
|
|
1034
|
-
end
|
|
1035
|
-
|
|
1036
|
-
-- Truncate long messages to prevent overly large payloads.
|
|
1037
|
-
local msg = string.sub(msg, 1, 100)
|
|
1038
|
-
|
|
1039
|
-
pcall(function()
|
|
1040
|
-
self._session:post(
|
|
1041
|
-
"/roblox/logservice",
|
|
1042
|
-
{
|
|
1043
|
-
["server_id"] = self._session.jobID,
|
|
1044
|
-
["message"] = msg,
|
|
1045
|
-
["type"] = convertedType
|
|
1046
|
-
}
|
|
1047
|
-
)
|
|
1048
|
-
end)
|
|
1049
|
-
end
|
|
1050
|
-
|
|
1051
|
-
--[[
|
|
1052
|
-
@method Zyntex:Log
|
|
1053
|
-
@description Logs a custom message to the Zyntex servers. This is useful for tracking specific
|
|
1054
|
-
game events. The log will appear in the main logs, the server's log tab, and the associated player's log tab if a player is provided.
|
|
1055
|
-
|
|
1056
|
-
@param self Zyntex
|
|
1057
|
-
@param message string -- The custom message to log.
|
|
1058
|
-
@param player (Player? | number?) -- Optional. The Player or UserId to associate with this log entry.
|
|
1059
|
-
@return boolean -- Returns `true` on success.
|
|
1060
|
-
]]
|
|
1061
|
-
function Zyntex.Log(self: Zyntex, message: string, player: Player? | number?)
|
|
1062
|
-
if self._config.debug then
|
|
1063
|
-
print(`[Zyntex]: Posting log...`)
|
|
1064
|
-
end
|
|
1065
|
-
local playerId, playerName;
|
|
1066
|
-
if player then
|
|
1067
|
-
playerId = if typeof(player) == "Player" then player.UserId else player
|
|
1068
|
-
playerName = if typeof(player) == "Player" then player.Name else Players:GetPlayerByUserId(player).Name
|
|
1069
|
-
end
|
|
1070
|
-
|
|
1071
|
-
local payload = {
|
|
1072
|
-
["message"] = message
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if playerId then
|
|
1076
|
-
payload["player_id"] = playerId
|
|
1077
|
-
payload["player_name"] = playerName
|
|
1078
|
-
end
|
|
1079
|
-
|
|
1080
|
-
local res = self._session:post(
|
|
1081
|
-
`/roblox/log`,
|
|
1082
|
-
payload
|
|
1083
|
-
)
|
|
1084
|
-
|
|
1085
|
-
-- Under /push this is enqueued; no synchronous assert.
|
|
1086
|
-
|
|
1087
|
-
if self._config.debug then
|
|
1088
|
-
print(`[Zyntex]: Log posted succesfully.`)
|
|
1089
|
-
end
|
|
1090
|
-
|
|
1091
|
-
return true
|
|
1092
|
-
end
|
|
1093
|
-
|
|
1094
|
-
--[[
|
|
1095
|
-
@method Zyntex:ProcessPurchase
|
|
1096
|
-
@description Logs a player's in-game purchase. This should be wired up to `MarketplaceService.ProcessReceipt`
|
|
1097
|
-
to track player spending and LTV on the dashboard.
|
|
1098
|
-
|
|
1099
|
-
@param self Zyntex
|
|
1100
|
-
@param player (Player | number) -- The Player object or UserId who made the purchase.
|
|
1101
|
-
@param price number -- The price of the item in Robux.
|
|
1102
|
-
@param productName string -- The name of the product purchased. Using the name is preferred over the ID for clarity on the dashboard.
|
|
1103
|
-
@return boolean -- Returns `true` on success.
|
|
1104
|
-
]]
|
|
1105
|
-
function Zyntex.ProcessPurchase(self: Zyntex, player: Player | number, price: number, productName: string): boolean
|
|
1106
|
-
local res = self._session:post(
|
|
1107
|
-
`/roblox/players/{if type(player) == "number" then player else player.UserId}/robux-spent`,
|
|
1108
|
-
{
|
|
1109
|
-
["server_id"] = self._session.jobID,
|
|
1110
|
-
["robux_spent"] = price;
|
|
1111
|
-
["metadata"] = productName
|
|
1112
|
-
}
|
|
1113
|
-
)
|
|
1114
|
-
|
|
1115
|
-
return true
|
|
1116
|
-
end
|
|
1117
|
-
|
|
1118
|
-
--[[
|
|
1119
|
-
@constructor Zyntex.new
|
|
1120
|
-
@description Constructs the main Zyntex client object and validates the API connection.
|
|
1121
|
-
It also checks if the client version is up-to-date with the latest version recommended by the backend.
|
|
1122
|
-
|
|
1123
|
-
@param gameToken string -- The unique game token obtained from the Zyntex dashboard.
|
|
1124
|
-
@return Zyntex -- The newly created Zyntex instance.
|
|
1125
|
-
]]
|
|
1126
|
-
function Zyntex.new(gameToken: string): Zyntex
|
|
1127
|
-
local self = {}
|
|
1128
|
-
|
|
1129
|
-
-- Pass JobId to enable the API push buffer/flush worker
|
|
1130
|
-
self._session = API.new(gameToken)
|
|
1131
|
-
self._config = {}
|
|
1132
|
-
self._events = {}
|
|
1133
|
-
self._functions = {}
|
|
1134
|
-
self._listeners = {}
|
|
1135
|
-
self._pendingJoins = {}
|
|
1136
|
-
self._version = CLIENT_VERSION
|
|
1137
|
-
|
|
1138
|
-
local current_version = self._session:get("/roblox/latest-client-version") -- direct HTTP (not push)
|
|
1139
|
-
assert(current_version and current_version.success, `Zyntex API is currently down.`)
|
|
1140
|
-
if (current_version.data ~= CLIENT_VERSION) then
|
|
1141
|
-
warn(`[Zyntex]: Your client is outdated, please download the latest version. current: v{CLIENT_VERSION}; latest: v{current_version.data}`)
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
|
-
-- Inside Zyntex.init, right after `self._session` is created
|
|
1145
|
-
local function handlePush(data)
|
|
1146
|
-
-- reuse the same logic currently inside Zyntex.poll
|
|
1147
|
-
if data and data.results then
|
|
1148
|
-
for _, result in ipairs(data.results) do
|
|
1149
|
-
if result.method == "POST /roblox/players" and result.rid then
|
|
1150
|
-
local player = self._pendingJoins[result.rid]
|
|
1151
|
-
if not player then continue end
|
|
1152
|
-
|
|
1153
|
-
if not result.success and typeof(result.error) == "string" then
|
|
1154
|
-
local codeStr, reason = string.match(
|
|
1155
|
-
result.error,
|
|
1156
|
-
"^(%d+)%s*:%s*(.+)$"
|
|
1157
|
-
)
|
|
1158
|
-
local code = tonumber(codeStr)
|
|
1159
|
-
reason = reason or "Moderation"
|
|
1160
|
-
|
|
1161
|
-
if code == 403 then
|
|
1162
|
-
local p = Players:GetPlayerByUserId(player.UserId)
|
|
1163
|
-
if p then p:Kick(reason) end
|
|
1164
|
-
elseif code == 401 then
|
|
1165
|
-
muteUserId(player.UserId)
|
|
1166
|
-
end
|
|
1167
|
-
end
|
|
1168
|
-
self._pendingJoins[result.rid] = nil
|
|
1169
|
-
end
|
|
1170
|
-
end
|
|
1171
|
-
end
|
|
1172
|
-
end
|
|
1173
|
-
|
|
1174
|
-
-- register the universal handler
|
|
1175
|
-
self._session:onFlush(handlePush)
|
|
1176
|
-
|
|
1177
|
-
return setmetatable(self, Zyntex)
|
|
1178
|
-
end
|
|
1179
|
-
|
|
1180
|
-
--[[
|
|
1181
|
-
@function randomUsername
|
|
1182
|
-
@description Generates a random username for simulation purposes.
|
|
1183
|
-
]]
|
|
1184
|
-
local function randomUsername()
|
|
1185
|
-
return "User_" .. math.random(1000, 9999)
|
|
1186
|
-
end
|
|
1187
|
-
|
|
1188
|
-
--[[
|
|
1189
|
-
@function simulateActivity
|
|
1190
|
-
@description When `config.simulate` is true, this function creates fake player join/leave events
|
|
1191
|
-
to help test the system in Studio without needing real players.
|
|
1192
|
-
]]
|
|
1193
|
-
local function simulateActivity(self)
|
|
1194
|
-
local fakePlayers = {}
|
|
1195
|
-
|
|
1196
|
-
while true do
|
|
1197
|
-
task.wait(math.random(0.1, 10))
|
|
1198
|
-
|
|
1199
|
-
local action = math.random(1, 4)
|
|
1200
|
-
|
|
1201
|
-
if action == 1 or action == 2 then
|
|
1202
|
-
-- Simulate Player Join
|
|
1203
|
-
local name = randomUsername()
|
|
1204
|
-
local id = 16054156146
|
|
1205
|
-
local fakePlayer = {
|
|
1206
|
-
Name = name,
|
|
1207
|
-
UserId = id
|
|
1208
|
-
}
|
|
1209
|
-
table.insert(fakePlayers, fakePlayer)
|
|
1210
|
-
pcall(onPlayerAdd, self, fakePlayer)
|
|
1211
|
-
|
|
1212
|
-
elseif (action == 3 or action == 4) and #fakePlayers > 0 then
|
|
1213
|
-
-- Simulate Player Leave
|
|
1214
|
-
local index = math.random(1, #fakePlayers)
|
|
1215
|
-
local fakePlayer = table.remove(fakePlayers, index)
|
|
1216
|
-
pcall(onPlayerRemove, self, fakePlayer)
|
|
1217
|
-
end
|
|
1218
|
-
end
|
|
1219
|
-
end
|
|
1220
|
-
|
|
1221
|
-
--[[
|
|
1222
|
-
@method Zyntex:GetPlayerInfo
|
|
1223
|
-
@description Fetches and returns player information, such as reputation, total robux spent, and total time played.
|
|
1224
|
-
|
|
1225
|
-
@param self Zyntex
|
|
1226
|
-
@param player Player|number -- Either a Player object or the player's UserId. If UserId, the player does not have to be in the server.
|
|
1227
|
-
]]
|
|
1228
|
-
function Zyntex.GetPlayerInfo(self: Zyntex, player: Player | number): {player: ZyntexPlayer, total_robux_spent: number, total_time_played: number}
|
|
1229
|
-
local res = self._session:get(`/roblox/players/{if type(player) == "number" then player else player.UserId}`)
|
|
1230
|
-
|
|
1231
|
-
assert(res.success, `Failed when attempting to fetch player: {res.user_message}`)
|
|
1232
|
-
|
|
1233
|
-
local playerInfo = res.data
|
|
1234
|
-
local playerInfoRaw = res.data.player
|
|
1235
|
-
|
|
1236
|
-
playerInfo["player"] = ZyntexPlayer.new(
|
|
1237
|
-
playerInfoRaw.id,
|
|
1238
|
-
playerInfoRaw.name,
|
|
1239
|
-
playerInfoRaw.avatar_url,
|
|
1240
|
-
playerInfoRaw.avatar_url_cache_expiry,
|
|
1241
|
-
playerInfoRaw.reputation,
|
|
1242
|
-
playerInfoRaw.raw_reputation
|
|
1243
|
-
)
|
|
1244
|
-
|
|
1245
|
-
return playerInfo
|
|
1246
|
-
end
|
|
1247
|
-
|
|
1248
|
-
--[[
|
|
1249
|
-
@method Zyntex:OnModeration
|
|
1250
|
-
@description Creates a hook to the moderation action.
|
|
1251
|
-
|
|
1252
|
-
@param self Zyntex
|
|
1253
|
-
@param moderationType string -- Returns a moderationType. Either 'ban', 'mute', or 'kick'.
|
|
1254
|
-
@param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the moderation occurs.
|
|
1255
|
-
]]
|
|
1256
|
-
function Zyntex.OnModeration(self: Zyntex, moderationType: string, callback: (player: number, reason: string) -> nil)
|
|
1257
|
-
table.insert(self._listeners[moderationType], callback)
|
|
1258
|
-
end
|
|
1259
|
-
|
|
1260
|
-
--[[
|
|
1261
|
-
@method Zyntex:OnModeration
|
|
1262
|
-
@description Creates a hook to the 'kick' mo eration action.
|
|
1263
|
-
|
|
1264
|
-
@param self Zyntex
|
|
1265
|
-
@param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the kick occurs.
|
|
1266
|
-
]]
|
|
1267
|
-
function Zyntex.OnKick(self: Zyntex, callback: (player: number, reason: string) -> nil)
|
|
1268
|
-
return self:OnModeration("kick", callback)
|
|
1269
|
-
end
|
|
1270
|
-
|
|
1271
|
-
--[[
|
|
1272
|
-
@method Zyntex:OnModeration
|
|
1273
|
-
@description Creates a hook to the 'ban' moderation action.
|
|
1274
|
-
|
|
1275
|
-
@param self Zyntex
|
|
1276
|
-
@param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the ban occurs.
|
|
1277
|
-
]]
|
|
1278
|
-
function Zyntex.OnBan(self: Zyntex, callback: (player: number, reason: string) -> nil)
|
|
1279
|
-
return self:OnModeration("ban", callback)
|
|
1280
|
-
end
|
|
1281
|
-
|
|
1282
|
-
--[[
|
|
1283
|
-
@method Zyntex:OnMute
|
|
1284
|
-
@description Creates a hook to the 'mute' moderation action.
|
|
1285
|
-
|
|
1286
|
-
@param self Zyntex
|
|
1287
|
-
@param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the mute occurs.
|
|
1288
|
-
]]
|
|
1289
|
-
function Zyntex.OnMute(self: Zyntex, callback: (player: number, reason: string) -> nil)
|
|
1290
|
-
return self:OnModeration("mute", callback)
|
|
1291
|
-
end
|
|
1292
|
-
|
|
1293
|
-
--[[
|
|
1294
|
-
@method Zyntex:init
|
|
1295
|
-
@description The main entry point to start the Zyntex client. This function initializes all core processes:
|
|
1296
|
-
it registers the server with the Zyntex backend, starts the status update and polling loops,
|
|
1297
|
-
sets up event listeners, and connects player tracking signals.
|
|
1298
|
-
|
|
1299
|
-
@param self Zyntex
|
|
1300
|
-
@param config TYPES.Config -- A configuration table that controls client behavior (e.g., `debug`, `simulate`).
|
|
1301
|
-
]]
|
|
1302
|
-
function Zyntex.init(self: Zyntex, config: TYPES.Config)
|
|
1303
|
-
self._config = config
|
|
1304
|
-
|
|
1305
|
-
local since = now()
|
|
1306
|
-
|
|
1307
|
-
if config.debug then
|
|
1308
|
-
print("[Zyntex]: Zyntex.init")
|
|
1309
|
-
print("[Zyntex]: Initializing server...")
|
|
1310
|
-
end
|
|
1311
|
-
|
|
1312
|
-
--// Initial server creation (ENQUEUED for /roblox/push)
|
|
1313
|
-
local privateServerId = game.PrivateServerId
|
|
1314
|
-
local privateServerParameter = ""
|
|
1315
|
-
if privateServerId ~= "" and game.PrivateServerOwnerId ~= 0 then
|
|
1316
|
-
privateServerParameter = privateServerId
|
|
1317
|
-
end
|
|
1318
|
-
self._session:post(
|
|
1319
|
-
"/roblox/servers",
|
|
1320
|
-
{
|
|
1321
|
-
["server_id"] = self._session.jobID,
|
|
1322
|
-
["version"] = game.PlaceVersion,
|
|
1323
|
-
["isPrivate"] = privateServerParameter
|
|
1324
|
-
}
|
|
1325
|
-
)
|
|
1326
|
-
|
|
1327
|
-
-- FIRST FLUSH: create server + fetch manifest + initial listen data + moderation lists
|
|
1328
|
-
local firstPush = self._session:Flush(since)
|
|
1329
|
-
if not firstPush or not firstPush.success then
|
|
1330
|
-
error(`Error when attempting to initialize server via /push`)
|
|
1331
|
-
end
|
|
1332
|
-
local manifest = firstPush.data and firstPush.data.manifest
|
|
1333
|
-
local listen_data = firstPush.data and firstPush.data.listen_data
|
|
1334
|
-
|
|
1335
|
-
-- Materialize functions + events from manifest (from /push)
|
|
1336
|
-
if manifest then
|
|
1337
|
-
for _,v in pairs(manifest) do
|
|
1338
|
-
if v.type == "function" then
|
|
1339
|
-
table.insert(self._functions, Function.new(
|
|
1340
|
-
self,
|
|
1341
|
-
v.id,
|
|
1342
|
-
v.name,
|
|
1343
|
-
v.description or "",
|
|
1344
|
-
v.parameters or {}
|
|
1345
|
-
))
|
|
1346
|
-
elseif v.type == "event" then
|
|
1347
|
-
table.insert(self._events, Event.new(
|
|
1348
|
-
self,
|
|
1349
|
-
v.id,
|
|
1350
|
-
v.name,
|
|
1351
|
-
v.description or "",
|
|
1352
|
-
v.data or {},
|
|
1353
|
-
false
|
|
1354
|
-
))
|
|
1355
|
-
end
|
|
1356
|
-
end
|
|
1357
|
-
end
|
|
1358
|
-
|
|
1359
|
-
--// Capture and send historical logs from before the client initialized.
|
|
1360
|
-
-- Don't want to send 300 messages all at once :)
|
|
1361
|
-
task.spawn(function()
|
|
1362
|
-
local history = LogService:GetLogHistory()
|
|
1363
|
-
local delay = 0
|
|
1364
|
-
if #history > 5 then
|
|
1365
|
-
delay = 1
|
|
1366
|
-
end
|
|
1367
|
-
-- Avoid sending an excessive number of historical logs.
|
|
1368
|
-
if #history > 30 then
|
|
1369
|
-
return
|
|
1370
|
-
end
|
|
1371
|
-
for _,message in pairs(history) do
|
|
1372
|
-
self:logServiceMessage(message.message, message.messageType)
|
|
1373
|
-
task.wait(delay)
|
|
1374
|
-
end
|
|
1375
|
-
end)
|
|
1376
|
-
|
|
1377
|
-
--// Connect live LogService listener.
|
|
1378
|
-
LogService.MessageOut:Connect(function(msg: string, type: Enum.MessageType)
|
|
1379
|
-
self:logServiceMessage(msg, type)
|
|
1380
|
-
end)
|
|
1381
|
-
|
|
1382
|
-
if config.debug then
|
|
1383
|
-
print("[Zyntex]: Server initialized")
|
|
1384
|
-
end
|
|
1385
|
-
|
|
1386
|
-
--// On server shutdown, notify the Zyntex backend.
|
|
1387
|
-
game:BindToClose(function()
|
|
1388
|
-
if config.debug then
|
|
1389
|
-
print("[Zyntex]: Server closing...")
|
|
1390
|
-
end
|
|
1391
|
-
sendingStatus = true
|
|
1392
|
-
self._session:delete("/roblox/servers", { ["server_id"] = self._session.jobID })
|
|
1393
|
-
self._session:Flush()
|
|
1394
|
-
|
|
1395
|
-
-- Clean up pending joins on shutdown
|
|
1396
|
-
local pendingCount = 0
|
|
1397
|
-
for rid, _ in pairs(self._pendingJoins) do
|
|
1398
|
-
pendingCount = pendingCount + 1
|
|
1399
|
-
end
|
|
1400
|
-
if pendingCount > 0 and config.debug then
|
|
1401
|
-
print(("[Zyntex]: Cleaning up %d pending joins on shutdown"):format(pendingCount))
|
|
1402
|
-
end
|
|
1403
|
-
self._pendingJoins = {}
|
|
1404
|
-
end)
|
|
1405
|
-
|
|
1406
|
-
--// Start the background processes.
|
|
1407
|
-
task.spawn(statusUpdateLoop, self)
|
|
1408
|
-
task.spawn(self:poll(since)) -- poll now uses /roblox/push flush
|
|
1409
|
-
|
|
1410
|
-
-- (No direct /manifest fetch: handled via first /push response)
|
|
1411
|
-
|
|
1412
|
-
--// Handle server shutdown action from the dashboard.
|
|
1413
|
-
self._listeners["shutdown"] = {}
|
|
1414
|
-
table.insert(self._listeners["shutdown"], function(action)
|
|
1415
|
-
local reason = action.metadata
|
|
1416
|
-
|
|
1417
|
-
-- Kick all current and future players with the provided reason.
|
|
1418
|
-
for _,player in pairs(Players:GetPlayers()) do
|
|
1419
|
-
player:Kick(reason)
|
|
1420
|
-
end
|
|
1421
|
-
|
|
1422
|
-
Players.PlayerAdded:Connect(function(player)
|
|
1423
|
-
player:Kick(reason)
|
|
1424
|
-
end)
|
|
1425
|
-
|
|
1426
|
-
-- Notify the dashboard that the action has been fulfilled.
|
|
1427
|
-
local res = self._session:post(
|
|
1428
|
-
`/roblox/actions/{action.id}/fulfill`,
|
|
1429
|
-
{},
|
|
1430
|
-
true
|
|
1431
|
-
)
|
|
1432
|
-
|
|
1433
|
-
if not res.success then
|
|
1434
|
-
warn(`[Zyntex]: Server shutdown fulfillment failed: {res.user_message}`)
|
|
1435
|
-
end
|
|
1436
|
-
|
|
1437
|
-
if config.debug then
|
|
1438
|
-
print(`[Zyntex]: Server shutdown fulfilled.`)
|
|
1439
|
-
end
|
|
1440
|
-
end)
|
|
1441
|
-
|
|
1442
|
-
--// Handle remote code execution (RCE) action from the dashboard.
|
|
1443
|
-
self._listeners["rce"] = {}
|
|
1444
|
-
table.insert(self._listeners["rce"], function(action)
|
|
1445
|
-
local code = action.metadata
|
|
1446
|
-
|
|
1447
|
-
if config.debug then
|
|
1448
|
-
print(`[Zyntex]: Fulfilling RCE request...`)
|
|
1449
|
-
end
|
|
1450
|
-
|
|
1451
|
-
-- Execute the code in a protected call to prevent crashes.
|
|
1452
|
-
task.spawn(function()
|
|
1453
|
-
local success,data = pcall(function()
|
|
1454
|
-
local executable,msg = require(script.Parent:FindFirstChild("Loadstring") :: ModuleScript)(code)
|
|
1455
|
-
executable()
|
|
1456
|
-
end)
|
|
1457
|
-
|
|
1458
|
-
if not success then
|
|
1459
|
-
warn(`RCE Failure: {data}`)
|
|
1460
|
-
end
|
|
1461
|
-
end)
|
|
1462
|
-
|
|
1463
|
-
local res = self._session:post(
|
|
1464
|
-
`/roblox/actions/{action.id}/fulfill`,
|
|
1465
|
-
{},
|
|
1466
|
-
true
|
|
1467
|
-
)
|
|
1468
|
-
|
|
1469
|
-
if config.debug then
|
|
1470
|
-
print(`[Zyntex]: RCE fulfilled.`)
|
|
1471
|
-
end
|
|
1472
|
-
end)
|
|
1473
|
-
|
|
1474
|
-
--// Handle system chat message action from the dashboard.
|
|
1475
|
-
self._listeners["chat"] = {}
|
|
1476
|
-
table.insert(self._listeners["chat"], function(action)
|
|
1477
|
-
if config.debug then
|
|
1478
|
-
print(`[Zyntex]: Fulfilling chat request...`)
|
|
1479
|
-
end
|
|
1480
|
-
|
|
1481
|
-
-- Fire a remote event to all clients to display a system message.
|
|
1482
|
-
game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events"):FindFirstChild("SystemChat"):FireAllClients(action.metadata)
|
|
1483
|
-
|
|
1484
|
-
local res = self._session:post(
|
|
1485
|
-
`/roblox/actions/{action.id}/fulfill`,
|
|
1486
|
-
{},
|
|
1487
|
-
true
|
|
1488
|
-
)
|
|
1489
|
-
|
|
1490
|
-
if not res.success then
|
|
1491
|
-
warn(`[Zyntex]: Chat fulfillment failed: {res.user_message}`)
|
|
1492
|
-
end
|
|
1493
|
-
|
|
1494
|
-
if config.debug then
|
|
1495
|
-
print(`[Zyntex]: Chat fulfilled.`)
|
|
1496
|
-
end
|
|
1497
|
-
end)
|
|
1498
|
-
|
|
1499
|
-
--// Handle moderation action from the dashboard.
|
|
1500
|
-
self._listeners["moderation"] = {}
|
|
1501
|
-
self._listeners["ban"] = {}
|
|
1502
|
-
self._listeners["mute"] = {}
|
|
1503
|
-
self._listeners["kick"] = {}
|
|
1504
|
-
self._listeners["report"] = {}
|
|
1505
|
-
table.insert(self._listeners["moderation"], function(action)
|
|
1506
|
-
if config.debug then
|
|
1507
|
-
print(`[Zyntex]: Fulfilling moderation request...`)
|
|
1508
|
-
print(`[Zyntex]: {action}`)
|
|
1509
|
-
end
|
|
1510
|
-
|
|
1511
|
-
local fullfilled = false
|
|
1512
|
-
|
|
1513
|
-
local type = action.metadata.type
|
|
1514
|
-
local player_id = action.metadata.player_id
|
|
1515
|
-
local reason = action.metadata.reason
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
if #self._listeners[type] == 0 then
|
|
1519
|
-
if type == "ban" or type == "kick" then
|
|
1520
|
-
local player = Players:GetPlayerByUserId(player_id)
|
|
1521
|
-
if player then
|
|
1522
|
-
player:Kick(reason)
|
|
1523
|
-
fullfilled = true
|
|
1524
|
-
end
|
|
1525
|
-
end
|
|
1526
|
-
|
|
1527
|
-
if type == "mute" then
|
|
1528
|
-
local player = Players:GetPlayerByUserId(player_id)
|
|
1529
|
-
if player then
|
|
1530
|
-
muteUserId(player.UserId)
|
|
1531
|
-
fullfilled = true
|
|
1532
|
-
end
|
|
1533
|
-
end
|
|
1534
|
-
else
|
|
1535
|
-
for _,listener in self._listeners[type] do
|
|
1536
|
-
listener(player_id, reason)
|
|
1537
|
-
end
|
|
1538
|
-
fullfilled = true
|
|
1539
|
-
end
|
|
1540
|
-
|
|
1541
|
-
if fullfilled then
|
|
1542
|
-
local res = self._session:post(
|
|
1543
|
-
`/roblox/actions/{action.id}/fulfill`,
|
|
1544
|
-
{},
|
|
1545
|
-
true
|
|
1546
|
-
)
|
|
1547
|
-
|
|
1548
|
-
if not res.success then
|
|
1549
|
-
warn(`[Zyntex]: Moderation fulfillment failed: {res.user_message}`)
|
|
1550
|
-
end
|
|
1551
|
-
end
|
|
1552
|
-
|
|
1553
|
-
if config.debug then
|
|
1554
|
-
print(`[Zyntex]: Moderation request fulfilled.`)
|
|
1555
|
-
end
|
|
1556
|
-
end)
|
|
1557
|
-
|
|
1558
|
-
--// If simulation is enabled in the config, start the activity simulator.
|
|
1559
|
-
if config.simulate then
|
|
1560
|
-
task.spawn(function()
|
|
1561
|
-
simulateActivity(self)
|
|
1562
|
-
end)
|
|
1563
|
-
end
|
|
1564
|
-
|
|
1565
|
-
--// Connect player join/leave listeners.
|
|
1566
|
-
Players.PlayerAdded:Connect(function(player: Player)
|
|
1567
|
-
return onPlayerAdd(self, player)
|
|
1568
|
-
end)
|
|
1569
|
-
|
|
1570
|
-
--// Ensure any players already present at initialization are registered.
|
|
1571
|
-
for i,player in Players:GetPlayers() do
|
|
1572
|
-
onPlayerAdd(self, player)
|
|
1573
|
-
end
|
|
1574
|
-
|
|
1575
|
-
Players.PlayerRemoving:Connect(function(player: Player)
|
|
1576
|
-
return onPlayerRemove(self, player)
|
|
1577
|
-
end)
|
|
1578
|
-
end
|
|
1579
|
-
|
|
1580
|
-
--[[
|
|
1581
|
-
@method Zyntex:link
|
|
1582
|
-
@description A utility function used during initial setup. When called, it attempts
|
|
1583
|
-
to link the provided game token to the user's Roblox account via the Zyntex backend.
|
|
1584
|
-
This is typically only run once from the command bar in Studio.
|
|
1585
|
-
]]
|
|
1586
|
-
function Zyntex.link(self: Zyntex)
|
|
1587
|
-
local response = self._session:post("/links/games/link")
|
|
1588
|
-
|
|
1589
|
-
if response.success == false then
|
|
1590
|
-
if response.statusCode == 404 then
|
|
1591
|
-
error(`Game token "{self._session.gameToken}" not found. Please make sure you pasted the full linking script from the site.`)
|
|
1592
|
-
end
|
|
1593
|
-
error(`Linking failed: {response.user_message}`)
|
|
1594
|
-
end
|
|
1595
|
-
|
|
1596
|
-
print(`[Zyntex]: Linking success!`)
|
|
1597
|
-
end
|
|
1598
|
-
|
|
1599
|
-
--[[
|
|
1600
|
-
@method Zyntex:Telemetry
|
|
1601
|
-
@description Constructs a new Telemetry object used for prometheus-style metrics.
|
|
1602
|
-
@param flushEvery number? -- How often to flush the buffer in seconds. Default is 10,
|
|
1603
|
-
which is the minimum to not get ratelimited.
|
|
1604
|
-
@param registryName string? -- The name of the registry. Default is "default"
|
|
1605
|
-
]]
|
|
1606
|
-
function Zyntex.Telemetry(self: Zyntex, flushEvery: number?, registryName: string?)
|
|
1607
|
-
if not flushEvery then
|
|
1608
|
-
flushEvery = 10
|
|
1609
|
-
end
|
|
1610
|
-
assert(flushEvery >= 10, `flushEvery must be no less than 10`)
|
|
1611
|
-
return Telemetry.new(
|
|
1612
|
-
registryName,
|
|
1613
|
-
flushEvery,
|
|
1614
|
-
self._session
|
|
1615
|
-
)
|
|
1616
|
-
end
|
|
1617
|
-
|
|
1618
|
-
--[[
|
|
1619
|
-
@method Zyntex:Experiments
|
|
1620
|
-
@description Constructs a new Experiments object used for A/B testing.
|
|
1621
|
-
@param experimentId string -- The ID of the experiment, i.e. "new_shop_ui". Set when creating experiment.
|
|
1622
|
-
@return Experiment? -- The Experiment, if exists and is not Archived
|
|
1623
|
-
]]
|
|
1624
|
-
function Zyntex.GetExperiment(self: Zyntex, experimentId: string): Experiments.Experiment?
|
|
1625
|
-
return Experiments.new(
|
|
1626
|
-
self._session,
|
|
1627
|
-
experimentId
|
|
1628
|
-
)
|
|
1629
|
-
end
|
|
1630
|
-
|
|
1631
|
-
return Zyntex
|