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