monitor/core/routes/player/actions.ts
2025-04-16 22:30:27 +07:00

365 lines
11 KiB
TypeScript

const modulename = 'WebServer:PlayerActions';
import humanizeDuration, { Unit } from 'humanize-duration';
import playerResolver from '@lib/player/playerResolver';
import { GenericApiResp } from '@shared/genericApiTypes';
import { PlayerClass, ServerPlayer } from '@lib/player/playerClasses';
import { anyUndefined, calcExpirationFromDuration } from '@lib/misc';
import consoleFactory from '@lib/console';
import { AuthedCtx } from '@modules/WebServer/ctxTypes';
import { SYM_CURRENT_MUTEX } from '@lib/symbols';
const console = consoleFactory(modulename);
/**
* Actions route for the player modal
*/
export default async function PlayerActions(ctx: AuthedCtx) {
//Sanity check
if (anyUndefined(ctx.params.action)) {
return ctx.utils.error(400, 'Invalid Request');
}
const action = ctx.params.action;
const { mutex, netid, license } = ctx.query;
const sendTypedResp = (data: GenericApiResp) => ctx.send(data);
//Finding the player
let player;
try {
const refMutex = mutex === 'current' ? SYM_CURRENT_MUTEX : mutex;
player = playerResolver(refMutex, parseInt((netid as string)), license);
} catch (error) {
return sendTypedResp({ error: (error as Error).message });
}
//Delegate to the specific action handler
if (action === 'save_note') {
return sendTypedResp(await handleSaveNote(ctx, player));
} else if (action === 'warn') {
return sendTypedResp(await handleWarning(ctx, player));
} else if (action === 'ban') {
return sendTypedResp(await handleBan(ctx, player));
} else if (action === 'whitelist') {
return sendTypedResp(await handleSetWhitelist(ctx, player));
} else if (action === 'message') {
return sendTypedResp(await handleDirectMessage(ctx, player));
} else if (action === 'kick') {
return sendTypedResp(await handleKick(ctx, player));
} else {
return sendTypedResp({ error: 'unknown action' });
}
};
/**
* Handle Save Note (open to all admins)
*/
async function handleSaveNote(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (anyUndefined(
ctx.request.body,
ctx.request.body.note,
)) {
return { error: 'Invalid request.' };
}
const note = ctx.request.body.note.trim();
try {
player.setNote(note, ctx.admin.name);
ctx.admin.logAction(`Set notes for ${player.license}`);
return { success: true };
} catch (error) {
return { error: `Failed to save note: ${(error as Error).message}` };
}
}
/**
* Handle Send Warning
*/
async function handleWarning(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (anyUndefined(
ctx.request.body,
ctx.request.body.reason,
)) {
return { error: 'Invalid request.' };
}
const reason = ctx.request.body.reason.trim() || 'no reason provided';
//Check permissions
if (!ctx.admin.testPermission('players.warn', modulename)) {
return { error: 'You don\'t have permission to execute this action.' };
}
//Validating server & player
const allIds = player.getAllIdentifiers();
if (!allIds.length) {
return { error: 'Cannot warn a player with no identifiers.' };
}
//Register action
let actionId;
try {
actionId = txCore.database.actions.registerWarn(
allIds,
ctx.admin.name,
reason,
player.displayName,
);
} catch (error) {
return { error: `Failed to warn player: ${(error as Error).message}` };
}
ctx.admin.logAction(`Warned player "${player.displayName}": ${reason}`);
// Dispatch `txAdmin:events:playerWarned`
const eventSent = txCore.fxRunner.sendEvent('playerWarned', {
author: ctx.admin.name,
reason,
actionId,
targetNetId: (player instanceof ServerPlayer && player.isConnected) ? player.netid : null,
targetIds: allIds,
targetName: player.displayName,
});
if (eventSent) {
return { success: true };
} else {
return { error: `Warn saved, but likely failed to send the warn in game (stdin error).` };
}
}
/**
* Handle Banning command
*/
async function handleBan(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (
anyUndefined(
ctx.request.body,
ctx.request.body.duration,
ctx.request.body.reason,
)
) {
return { error: 'Invalid request.' };
}
const durationInput = ctx.request.body.duration.trim();
const reason = (ctx.request.body.reason as string).trim() || 'no reason provided';
//Calculating expiration/duration
let calcResults;
try {
calcResults = calcExpirationFromDuration(durationInput);
} catch (error) {
return { error: (error as Error).message };
}
const { expiration, duration } = calcResults;
//Check permissions
if (!ctx.admin.testPermission('players.ban', modulename)) {
return { error: 'You don\'t have permission to execute this action.' }
}
//Validating player - hwids.length can be zero
const allIds = player.getAllIdentifiers();
const allHwids = player.getAllHardwareIdentifiers();
if (!allIds.length) {
return { error: 'Cannot ban a player with no identifiers.' }
}
//Register action
let actionId;
try {
actionId = txCore.database.actions.registerBan(
allIds,
ctx.admin.name,
reason,
expiration,
player.displayName,
allHwids
);
} catch (error) {
return { error: `Failed to ban player: ${(error as Error).message}` };
}
ctx.admin.logAction(`Banned player "${player.displayName}": ${reason}`);
//No need to dispatch events if server is not online
if (txCore.fxRunner.isIdle) {
return { success: true };
}
//Prepare and send command
let kickMessage, durationTranslated;
const tOptions: any = {
author: txCore.adminStore.getAdminPublicName(ctx.admin.name, 'punishment'),
reason: reason,
};
if (expiration !== false && duration) {
durationTranslated = txCore.translator.tDuration(
duration * 1000,
{ units: ['d', 'h'] },
);
tOptions.expiration = durationTranslated;
kickMessage = txCore.translator.t('ban_messages.kick_temporary', tOptions);
} else {
durationTranslated = null;
kickMessage = txCore.translator.t('ban_messages.kick_permanent', tOptions);
}
// Dispatch `txAdmin:events:playerBanned`
const eventSent = txCore.fxRunner.sendEvent('playerBanned', {
author: ctx.admin.name,
reason,
actionId,
expiration,
durationInput,
durationTranslated,
targetNetId: (player instanceof ServerPlayer) ? player.netid : null,
targetIds: player.ids,
targetHwids: player.hwids,
targetName: player.displayName,
kickMessage,
});
if (eventSent) {
return { success: true };
} else {
return { error: `Player banned, but likely failed to kick player (stdin error).` };
}
}
/**
* Handle Player Whitelist Action
*/
async function handleSetWhitelist(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (anyUndefined(
ctx.request.body,
ctx.request.body.status,
)) {
return { error: 'Invalid request.' };
}
const status = (ctx.request.body.status === 'true' || ctx.request.body.status === true);
//Check permissions
if (!ctx.admin.testPermission('players.whitelist', modulename)) {
return { error: 'You don\'t have permission to execute this action.' }
}
try {
player.setWhitelist(status);
if (status) {
ctx.admin.logAction(`Added ${player.license} to the whitelist.`);
} else {
ctx.admin.logAction(`Removed ${player.license} from the whitelist.`);
}
// Dispatch `txAdmin:events:whitelistPlayer`
txCore.fxRunner.sendEvent('whitelistPlayer', {
action: status ? 'added' : 'removed',
license: player.license,
playerName: player.displayName,
adminName: ctx.admin.name,
});
return { success: true };
} catch (error) {
return { error: `Failed to save whitelist status: ${(error as Error).message}` };
}
}
/**
* Handle Direct Message Action
*/
async function handleDirectMessage(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (anyUndefined(
ctx.request.body,
ctx.request.body.message,
)) {
return { error: 'Invalid request.' };
}
const message = ctx.request.body.message.trim();
if (!message.length) {
return { error: 'Cannot send a DM with empty message.' };
}
//Check permissions
if (!ctx.admin.testPermission('players.direct_message', modulename)) {
return { error: 'You don\'t have permission to execute this action.' };
}
//Validating server & player
if (!txCore.fxRunner.child?.isAlive) {
return { error: 'The server is not running.' };
}
if (!(player instanceof ServerPlayer) || !player.isConnected) {
return { error: 'This player is not connected to the server.' };
}
try {
ctx.admin.logAction(`DM to "${player.displayName}": ${message}`);
// Dispatch `txAdmin:events:playerDirectMessage`
txCore.fxRunner.sendEvent('playerDirectMessage', {
target: player.netid,
author: ctx.admin.name,
message,
});
return { success: true };
} catch (error) {
return { error: `Failed to save dm player: ${(error as Error).message}` };
}
}
/**
* Handle Kick Action
*/
async function handleKick(ctx: AuthedCtx, player: PlayerClass): Promise<GenericApiResp> {
//Checking request
if (anyUndefined(
ctx.request.body,
ctx.request.body.reason,
)) {
return { error: 'Invalid request.' };
}
const kickReason = ctx.request.body.reason.trim() || txCore.translator.t('kick_messages.unknown_reason');
//Check permissions
if (!ctx.admin.testPermission('players.kick', modulename)) {
return { error: 'You don\'t have permission to execute this action.' };
}
//Validating server & player
if (!txCore.fxRunner.child?.isAlive) {
return { error: 'The server is not running.' };
}
if (!(player instanceof ServerPlayer) || !player.isConnected) {
return { error: 'This player is not connected to the server.' };
}
try {
ctx.admin.logAction(`Kicked "${player.displayName}": ${kickReason}`);
const dropMessage = txCore.translator.t(
'kick_messages.player',
{ reason: kickReason }
);
// Dispatch `txAdmin:events:playerKicked`
txCore.fxRunner.sendEvent('playerKicked', {
target: player.netid,
author: ctx.admin.name,
reason: kickReason,
dropMessage,
});
return { success: true };
} catch (error) {
return { error: `Failed to save kick player: ${(error as Error).message}` };
}
}