File transfer

This commit is contained in:
2026-06-11 20:33:44 -05:00
parent 2cb1d58264
commit b362357bcb
58 changed files with 5096 additions and 4 deletions

View File

@@ -0,0 +1,188 @@
<?php
/**
* Autocomplete Engine — resolves command suggestions from stored definitions + player cache.
*
* @author Ball Studios <https://git.balls.studio>
* @license MIT
*/
namespace Pterodactyl\Addons\AdvancedAdmin\Services\Console;
use Illuminate\Support\Facades\Cache;
use Pterodactyl\Addons\AdvancedAdmin\Models\CommandDefinition;
use Pterodactyl\Addons\AdvancedAdmin\Models\ConsolePlayerCache;
class AutocompleteEngine
{
/**
* Generate suggestions for a given query on a server.
*
* @return array<int, array{command: string, description: string, syntax: string, dangerous: bool, plugin: string}>
*/
public function suggest(int $serverId, ?int $nodeId, ?int $eggId, string $query): array
{
$query = $this->sanitize($query);
if (strlen($query) < 1) return [];
$definitions = $this->loadDefinitions($serverId, $nodeId, $eggId);
$players = $this->getOnlinePlayers($serverId);
// Determine if we're suggesting a command name or an argument
$parts = explode(' ', $query);
// No space yet → suggest command names
if (count($parts) === 1) {
return $this->suggestCommands($query, $definitions);
}
// Space present → suggest arguments for the matched command
$commandName = strtolower($parts[0]);
return $this->suggestArguments($commandName, $parts, $definitions, $players);
}
/**
* Sanitize query — prevent XSS/injection in rendered suggestions.
*/
private function sanitize(string $query): string
{
// Strip null bytes, control chars, HTML
$query = preg_replace('/[\x00-\x1F\x7F]/', '', $query);
$query = strip_tags($query);
return mb_substr(trim($query), 0, 256);
}
private function suggestCommands(string $prefix, array $definitions): array
{
$prefix = strtolower($prefix);
$results = [];
$max = config('advanced-admin.console.max_suggestions', 10);
foreach ($definitions as $def) {
$names = array_merge([$def['name']], $def['aliases'] ?? []);
foreach ($names as $name) {
if (str_starts_with(strtolower($name), $prefix)) {
$results[] = $this->formatSuggestion($def, $name);
if (count($results) >= $max) break 2;
}
}
}
return $results;
}
private function suggestArguments(string $cmd, array $parts, array $definitions, array $players): array
{
$definition = null;
foreach ($definitions as $def) {
$names = array_merge([$def['name']], $def['aliases'] ?? []);
if (in_array($cmd, array_map('strtolower', $names), true)) {
$definition = $def;
break;
}
}
if (!$definition) return [];
$argIndex = count($parts) - 2; // 0-based index of current argument
$args = $definition['args'] ?? [];
if (!isset($args[$argIndex])) return [];
$argDef = $args[$argIndex];
$partial = strtolower(end($parts));
$max = config('advanced-admin.console.max_suggestions', 10);
$results = [];
if ($argDef['type'] === 'player') {
foreach ($players as $player) {
if (str_starts_with(strtolower($player), $partial)) {
$results[] = ['command' => $player, 'description' => 'Online player', 'syntax' => '', 'dangerous' => false, 'plugin' => 'server'];
if (count($results) >= $max) break;
}
}
} elseif (!empty($argDef['suggestions'])) {
foreach ($argDef['suggestions'] as $s) {
if (str_starts_with(strtolower($s), $partial)) {
$results[] = ['command' => $s, 'description' => $argDef['name'] ?? '', 'syntax' => '', 'dangerous' => false, 'plugin' => ''];
if (count($results) >= $max) break;
}
}
}
// Return parent command suggestion with full syntax
if (empty($results)) {
$results[] = $this->formatSuggestion($definition, $definition['name']);
}
return $results;
}
private function formatSuggestion(array $def, string $displayName): array
{
$syntax = '/' . $displayName;
foreach ($def['args'] ?? [] as $arg) {
$syntax .= $arg['required'] ? " <{$arg['name']}>" : " [{$arg['name']}]";
}
return [
'command' => $displayName,
'description' => $def['description'] ?? '',
'syntax' => $syntax,
'dangerous' => $def['dangerous'] ?? false,
'danger_reason' => $def['dangerReason'] ?? null,
'plugin' => $def['plugin'] ?? 'vanilla',
'examples' => $def['examples'] ?? [],
];
}
/**
* Load command definitions for this server (server-specific → node → egg → global).
* Cached for 60 seconds per server.
*/
private function loadDefinitions(int $serverId, ?int $nodeId, ?int $eggId): array
{
$cacheKey = "addon_commands_{$serverId}_{$nodeId}_{$eggId}";
return Cache::remember($cacheKey, 60, function () use ($serverId, $nodeId, $eggId) {
// DB custom definitions
$dbDefs = CommandDefinition::query()
->where('is_active', true)
->where(function ($q) use ($serverId, $nodeId, $eggId) {
$q->whereNull('server_id')
->orWhere('server_id', $serverId);
})
->get()
->map(fn ($d) => $d->definition)
->toArray();
// Built-in JSON definitions from config/addon-commands/
$builtIn = $this->loadBuiltInDefinitions();
return array_merge($builtIn, $dbDefs);
});
}
private function loadBuiltInDefinitions(): array
{
$path = config('advanced-admin.console.definitions_path', base_path('config/addon-commands'));
$files = glob($path . '/*.json') ?: [];
$defs = [];
foreach ($files as $file) {
$raw = file_get_contents($file);
if (!$raw) continue;
$decoded = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) continue;
$defs = array_merge($defs, is_array($decoded[0] ?? null) ? $decoded : [$decoded]);
}
return $defs;
}
private function getOnlinePlayers(int $serverId): array
{
return ConsolePlayerCache::query()
->where('server_id', $serverId)
->where('last_seen_at', '>=', now()->subMinutes(
config('advanced-admin.console.player_cache_ttl_minutes', 30)
))
->pluck('player_name')
->toArray();
}
}