Server
Every server export, with examples.
All exports are server side.
screenshot#
Captures a single frame from a player's screen. The result stays on your server - nothing is uploaded anywhere.
exports.cube_monitoring:screenshot(targetId, handler)- targetId:
number - handler:
function(result, error)- result:
tableornilbase64:stringmimeType:string- alwaysimage/webp
- error:
stringornil
- result:
Returns the request id, or nil if the target is offline. The handler is always called exactly once - on success, on failure, or on timeout (Config.ScreenshotTimeout).
<u>Examples</u>
exports.cube_monitoring:screenshot(source, function(result, err)
if not result then
print('capture failed: ' .. err)
return
end
print('captured ' .. #result.base64 .. ' bytes of base64')
end)Write the frame next to your resource.
exports.cube_monitoring:screenshot(targetId, function(result, err)
if not result then return end
SaveResourceFile(
GetCurrentResourceName(),
('captures/%s-%s.webp'):format(targetId, os.time()),
result.base64,
-1
)
end)<Callout type="warning"> The data is base64. Decode it if you need a real image file on disk. </Callout>
Capture automatically when a player is reported.
RegisterNetEvent('myreports:create', function(targetId, reason)
local reporter = source
exports.cube_monitoring:screenshotToDiscord(targetId, function(url, err)
if not url then
print('report capture failed: ' .. err)
return
end
TriggerEvent('myreports:attach', reporter, url, reason)
end)
end)screenshotToDiscord#
Captures a frame and posts it straight to a Discord webhook. The handler receives the hosted attachment URL.
exports.cube_monitoring:screenshotToDiscord(targetId, handler, webhookUrl)- targetId:
number - handler:
function(url, error) - webhookUrl:
string(optional)- Falls back to
ServerConfig.Webhook
- Falls back to
exports.cube_monitoring:screenshotToDiscord(targetId, function(url, err)
if not url then
print('upload failed: ' .. err)
return
end
print('uploaded: ' .. url)
end)With an explicit webhook, to route different staff teams to different channels:
exports.cube_monitoring:screenshotToDiscord(
targetId,
function(url) print(url) end,
'https://discord.com/api/webhooks/...'
)uploadToDiscord#
Posts an already-captured file. Retries on rate limits and 5xx responses.
exports.cube_monitoring:uploadToDiscord(base64, filename, mimeType, content, handler, webhookUrl)- base64:
string - filename:
string - mimeType:
string - content:
string- message body, truncated to 2000 characters - handler:
function(url, error) - webhookUrl:
string(optional)
exports.cube_monitoring:uploadToDiscord(
clipBase64,
'clip.webm',
'video/webm',
'Clip of **Player** (`12`)',
function(url, err)
if url then print(url) end
end
)Discord rejects files over 8 MB on a standard webhook.
watch#
Opens a live stream. The admin's interface opens on its own.
exports.cube_monitoring:watch(viewerId, targetId)- viewerId:
number- the admin who will see the window - targetId:
number- the player being watched - returns:
stringornil- the stream id
Returns nil when either player is offline or a limit in Config was hit. Calling it again for a pair that is already streaming returns the existing id rather than opening a second window.
local streamId = exports.cube_monitoring:watch(adminId, suspectId)
if not streamId then
print('could not start the stream')
endstopWatch#
Closes one stream and notifies both sides.
exports.cube_monitoring:stopWatch(streamId)- streamId:
string - returns:
boolean- whether the stream existed
stopWatchingAs#
Closes every stream one admin has open.
local closed = exports.cube_monitoring:stopWatchingAs(adminId)
print(('closed %d streams'):format(closed))- viewerId:
number - returns:
number- how many were closed
Useful when an admin goes off duty:
RegisterNetEvent('myduty:off', function()
exports.cube_monitoring:stopWatchingAs(source)
end)getSessions#
Every live stream on the server.
for _, session in ipairs(exports.cube_monitoring:getSessions()) do
print(('%s: %s watching %s since %s'):format(
session.streamId, session.viewer, session.target, session.startedAt
))
end- returns:
table[]streamId:stringviewer:numbertarget:numberstartedAt:number- Unix timestamp
isAllowed#
Runs the same permission check the resource uses, so your own menus stay in sync.
if exports.cube_monitoring:isAllowed(source) then
-- show the monitoring button
end- src:
number - returns:
boolean
openPanel#
Opens the player list for an admin, without going through a command.
exports.cube_monitoring:openPanel(source)- src:
number - returns:
boolean
Handy for an admin menu button:
RegisterNetEvent('myadmin:openMonitoring', function()
local src = source
if not exports.cube_monitoring:isAllowed(src) then return end
exports.cube_monitoring:openPanel(src)
end)Logging watch sessions#
Built-in Discord logging is a config switch:
ServerConfig.Logs = {
enabled = true,
webhook = "https://discord.com/api/webhooks/...",
}For a database log instead, wrap the export:
local function watchAndLog(viewerId, targetId)
local streamId = exports.cube_monitoring:watch(viewerId, targetId)
if not streamId then return nil end
MySQL.insert.await(
'INSERT INTO monitoring_log (admin, target, at) VALUES (?, ?, NOW())',
{ GetPlayerIdentifier(viewerId, 0), GetPlayerIdentifier(targetId, 0) }
)
return streamId
endTo catch the in-game commands as well, add your logging inside HasPermission in server/function.lua - every entry point passes through it.