File transfer
This commit is contained in:
188
app/Addons/AdvancedAdmin/Services/Console/AutocompleteEngine.php
Normal file
188
app/Addons/AdvancedAdmin/Services/Console/AutocompleteEngine.php
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user