Skip to content

Commit 6daa80d

Browse files
authored
Merge pull request #212 from BlueprintFramework/remote-metadata
Add version update reminders
2 parents 10a5e18 + 4634387 commit 6daa80d

13 files changed

Lines changed: 299 additions & 29 deletions

File tree

app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515
namespace Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary;
1616

17-
use Illuminate\Support\Facades\DB;
18-
use Illuminate\Support\Collection;
1917
use Symfony\Component\Yaml\Yaml;
18+
use Illuminate\Support\Collection;
19+
use Illuminate\Support\Facades\DB;
20+
use Pterodactyl\Models\ExtensionCachedMetadata;
2021

2122
class BlueprintBaseLibrary
2223
{
@@ -294,6 +295,39 @@ public function extensionConfig(string $identifier): ?array
294295
return $conf;
295296
}
296297

298+
/**
299+
* Retrieves the stored metadata for an extension.
300+
*
301+
* This method checks if the given extension has available metadata and, if so,
302+
* returns the metadata. Please note that this metadata may be delayed, and the
303+
* format of which may change, so parse wisely.
304+
*
305+
* Requires the 'remote_metadata' flag to be set to true in Blueprint's settings.
306+
*
307+
* @param string $identifier Extension identifier to retrieve metadata for
308+
*
309+
* @return array|null The metadata array for the extension, or null if not available.
310+
*/
311+
public function extensionMetadata(string $identifier): ?array
312+
{
313+
$metadata = [];
314+
if (!$this->extension($identifier)) {
315+
return null;
316+
}
317+
if($this->dbGet('blueprint', 'flags:remote_metadata')) {
318+
$metadata = ExtensionCachedMetadata::whereIn('identifier', $this->extensions())
319+
->get()
320+
->keyBy('identifier')
321+
->map(fn($m) => $m->metadata)
322+
->toArray();
323+
324+
if(isset($metadata[$identifier])) {
325+
return $metadata[$identifier];
326+
}
327+
}
328+
return null;
329+
}
330+
297331
/**
298332
* Returns a Collection containing all installed extensions's configs.
299333
*
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
// This command fetches extension metadata on a schedule, to then provide administrators
4+
// with stuff like latest version info, and maybe more in the future.
5+
//
6+
// 1. make a request to blueprint.zip/api/extensions/latest
7+
// 2. figure out which extensions it should write metadata for
8+
// 3. build a json object for those extensions' metadata
9+
// 4. flush metadata table
10+
// 5. write metadata table
11+
12+
namespace Pterodactyl\Console\Commands\BlueprintFramework;
13+
14+
use Illuminate\Console\Command;
15+
use Illuminate\Support\Facades\DB;
16+
use Pterodactyl\Models\ExtensionCachedMetadata;
17+
use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService;
18+
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Console\BlueprintConsoleLibrary as BlueprintExtensionLibrary;
19+
20+
class MetadataCacheCommand extends Command
21+
{
22+
protected $description = 'Refreshes extension metadata';
23+
protected $signature = 'bp:meta';
24+
25+
public function __construct(
26+
private BlueprintPlaceholderService $PlaceholderService,
27+
private BlueprintExtensionLibrary $blueprint,
28+
) {
29+
parent::__construct();
30+
}
31+
32+
public function handle()
33+
{
34+
if(! $this->blueprint->dbGet("blueprint", "flags:remote_metadata")) {
35+
$this->error('remote_metadata flag set to false');
36+
return false;
37+
}
38+
39+
$now = now();
40+
$rows = [];
41+
$installedExtensions = $this->blueprint->extensions();
42+
43+
// get version info
44+
$context = stream_context_create(['http' => ['method' => 'GET', 'header' => 'User-Agent: BlueprintFramework']]);
45+
$remoteVersions = @file_get_contents(
46+
$this->PlaceholderService->api_url() . '/api/extensions/latest',
47+
false,
48+
$context
49+
);
50+
51+
if($remoteVersions) {
52+
$remoteVersionsData = json_decode($remoteVersions, true);
53+
}
54+
55+
if(! isset($remoteVersionsData)) {
56+
$this->error('failed to fetch extension versions');
57+
return false;
58+
}
59+
60+
foreach ($installedExtensions as $identifier) {
61+
if(! isset($remoteVersionsData[$identifier]) || ! is_scalar($remoteVersionsData[$identifier])) continue;
62+
63+
$local_extension = $this->blueprint->extensionConfig($identifier);
64+
65+
$rows[] = [
66+
'identifier' => $identifier,
67+
'metadata' => json_encode([
68+
'latest_version' => (string) $remoteVersionsData[$identifier],
69+
'local_version' => (string) $local_extension['info']['version'] ?? '',
70+
]),
71+
'fetched_at' => $now,
72+
];
73+
}
74+
75+
if (empty($rows)) {
76+
$this->info('no relevant data available, do you have any extensions installed?');
77+
return false;
78+
}
79+
80+
$table = (new ExtensionCachedMetadata())->getTable();
81+
82+
DB::transaction(function () use ($rows, $table) {
83+
DB::table($table)->delete();
84+
DB::table($table)->insert($rows);
85+
});
86+
87+
$this->info('updated extension cached metadata');
88+
}
89+
}

app/Console/Kernel.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ protected function schedule(Schedule $schedule): void
5454
$this->registerTelemetry($schedule);
5555
}
5656

57+
// ============================
58+
// BLUEPRINT SCHEDULES
59+
// ============================
60+
5761
// Blueprint telemetry
5862
$blueprint = app()->make(BlueprintExtensionLibrary::class);
5963
if ($blueprint->dbGet('blueprint', 'flags:telemetry_enabled', 0)) {
@@ -62,7 +66,9 @@ protected function schedule(Schedule $schedule): void
6266
}
6367

6468
// Blueprint-related utilities
65-
$schedule->command('bp:version:cache')->dailyAt(str_pad(rand(0, 23), 2, '0', STR_PAD_LEFT) . ':' . str_pad(rand(0, 59), 2, '0', STR_PAD_LEFT));
69+
$randTime = str_pad(rand(0, 23), 2, '0', STR_PAD_LEFT) . ':' . str_pad(rand(0, 59), 2, '0', STR_PAD_LEFT);
70+
$schedule->command('bp:version:cache')->dailyAt($randTime);
71+
$schedule->command('bp:meta')->dailyAt($randTime);
6672

6773
// Blueprint extension schedules
6874
GetExtensionSchedules::schedules($schedule);

app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
namespace Pterodactyl\Http\Controllers\Admin\Extensions\Blueprint;
44

5-
use Pterodactyl\Http\Controllers\Controller;
6-
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
75
use Illuminate\Http\RedirectResponse;
8-
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
96
use Database\Seeders\BlueprintSeeder;
7+
use Illuminate\Support\Facades\Artisan;
8+
use Pterodactyl\Http\Controllers\Controller;
9+
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
10+
use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface;
1011

11-
// FIXME: Move form request and remove controller.
1212
class BlueprintExtensionController extends Controller
1313
{
1414

@@ -26,10 +26,17 @@ public function __construct(
2626
*/
2727
public function update(BlueprintAdminFormRequest $request): RedirectResponse
2828
{
29+
$meta_flag = $this->settings->get('blueprint::flags:remote_metadata');
30+
2931
foreach ($request->validated() as $key => $value) {
3032
$this->settings->set('blueprint::' . $key, $value);
3133
}
3234

35+
// refresh meta if the flag has been altered
36+
if($meta_flag != $request->validated()['flags:remote_metadata']) {
37+
Artisan::call('bp:meta');
38+
}
39+
3340
return redirect()->route('admin.extensions');
3441
}
3542
}
@@ -41,11 +48,11 @@ public function rules(): array
4148
// Get schema to determine types
4249
$seeder = app(BlueprintSeeder::class);
4350
$schema = $seeder->getSchema();
44-
51+
4552
$rules = [];
4653
foreach ($schema['flags'] as $key => $config) {
4754
$flagPath = "flags:{$key}";
48-
55+
4956
// Build validation rules based on type
5057
switch ($config['type']) {
5158
case 'boolean':
@@ -62,7 +69,7 @@ public function rules(): array
6269
break;
6370
}
6471
}
65-
72+
6673
return $rules;
6774
}
68-
}
75+
}

app/Http/Controllers/Admin/ExtensionsController.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
namespace Pterodactyl\Http\Controllers\Admin;
44

5-
use Illuminate\Support\Facades\Artisan;
65
use Illuminate\View\View;
6+
use Database\Seeders\BlueprintSeeder;
7+
use Illuminate\Support\Facades\Artisan;
78
use Illuminate\View\Factory as ViewFactory;
89
use Pterodactyl\Http\Controllers\Controller;
10+
use Pterodactyl\Models\ExtensionCachedMetadata;
911
use Pterodactyl\Services\Helpers\SoftwareVersionService;
1012
use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService;
1113
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
12-
use Database\Seeders\BlueprintSeeder;
1314

1415
class ExtensionsController extends Controller
1516
{
@@ -32,7 +33,7 @@ public function index(): View
3233
{
3334
$configuration = $this->blueprint->dbGetMany('blueprint');
3435
$defaults = [];
35-
36+
3637
if (($configuration['internal:version:latest'] ?? false) === false) {
3738
Artisan::call('bp:version:cache');
3839
$latestBlueprintVersion = $this->blueprint->dbGet('blueprint', 'internal:version:latest');
@@ -47,14 +48,24 @@ public function index(): View
4748
}
4849
}
4950

51+
$metadata = [];
52+
if($this->blueprint->dbGet('blueprint', 'flags:remote_metadata')) {
53+
$metadata = ExtensionCachedMetadata::whereIn('identifier', $this->blueprint->extensions())
54+
->get()
55+
->keyBy('identifier')
56+
->map(fn($m) => $m->metadata)
57+
->toArray();
58+
}
59+
5060
return $this->view->make('admin.extensions', [
5161
'blueprint' => $this->blueprint,
5262
'PlaceholderService' => $this->PlaceholderService,
5363
'configuration' => $configuration,
5464
'latestBlueprintVersion' => $latestBlueprintVersion,
5565
'defaults' => $defaults,
5666
'seeder' => $this->seeder,
57-
67+
'metadata' => $metadata,
68+
5869
'version' => $this->version,
5970
'root' => "/admin/extensions",
6071
]);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Pterodactyl\Models;
4+
5+
class ExtensionCachedMetadata extends Model
6+
{
7+
protected $table = 'extension_cached_metadata';
8+
protected $casts = [
9+
'metadata' => 'array',
10+
'fetched_at' => 'datetime',
11+
];
12+
protected $fillable = ['identifier', 'metadata', 'fetched_at'];
13+
public $timestamps = true;
14+
15+
// return the latest_version for a given extension identifier, or null if not found
16+
public static function latestVersionFor(string $identifier): ?string
17+
{
18+
$row = static::where('identifier', $identifier)->first(['metadata']);
19+
20+
if (! $row) {
21+
return null;
22+
}
23+
24+
return $row->metadata['latest_version'] ?? null;
25+
}
26+
}

blueprint/extensions/blueprint/assets/blueprint.style.css

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
tag {
2727
display:inline-block;
28-
padding:3px;
29-
background-color:#505050;
30-
border-radius:5px;
28+
padding: 3px 5px;
29+
background-color: #4d5b69;
30+
border-radius:15px;
3131
font-size:12px;
3232
color:white;
3333
}
@@ -36,7 +36,7 @@ tag[mg-right] {margin-right:5px;}
3636
tag[red] {background-color:#ff4040;}
3737
tag[green] {background-color:#27b949;}
3838
tag[blue] {background-color:#288afb;}
39-
[ext-title]{display:flex; flex-direction:row; align-items:center;}
39+
[ext-title]{display:flex; flex-direction:row; align-items: end;}
4040

4141

4242
.btn-gray {
@@ -111,7 +111,18 @@ tag[blue] {background-color:#288afb;}
111111
width: calc( 100% - 15px );
112112
overflow: clip;
113113
text-overflow: ellipsis;
114-
opacity: .6;
114+
}
115+
.extension-btn-update {
116+
background-color: #194323;
117+
color: #3dd15f !important;
118+
border: 1px solid #265f33;
119+
font-weight: 700;
120+
border-radius: 12px;
121+
padding: 0 8px;
122+
display: inline-flex;
123+
flex-direction: row;
124+
gap: 5px;
125+
margin-left: 5px;
115126
}
116127
.extension-btn {
117128
background-color:#1f2933;
@@ -120,7 +131,6 @@ tag[blue] {background-color:#288afb;}
120131
height:calc(65px + 14px);
121132
padding: 0px !important;
122133
overflow:hidden;
123-
vertical-align:center;
124134
transition:background-color .2s;
125135
border-radius:8px;
126136
}
@@ -169,9 +179,15 @@ tag[blue] {background-color:#288afb;}
169179
margin-left: -7px;
170180
border-width: 7px;
171181
border-style: solid;
172-
border-color:
182+
border-color:
173183
transparent
174184
transparent
175185
#1f2933
176186
transparent;
177-
}
187+
}
188+
189+
@media screen and (width <= 600px) {
190+
.blueprint-extension-title-tag-icon {
191+
display: none;
192+
}
193+
}

database/Seeders/BlueprintSeeder.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class BlueprintSeeder extends Seeder
4545
'type' => 'boolean',
4646
'hidden' => false,
4747
],
48+
'remote_metadata' => [
49+
'default' => true,
50+
'type' => 'boolean',
51+
'hidden' => false,
52+
],
4853
'show_in_sidebar' => [
4954
'default' => false,
5055
'type' => 'boolean',

0 commit comments

Comments
 (0)