Asset related changes.

- Created configuration for Roblox cookie to be used with rate-limited Roblox apis.
- Autouploader APIs.
- Fillable Assets and AssetVersions.
- Indev auto uploader.
- Roblox asset tracker. This will prevent reuploads of already auto-uploaded assets.
- Update assets to use longText instead of string on descriptions.
- Modify view to be able to display newlines.
- Fix truncate on shop card titles.
- Create default thumbnails for deleted, pending, and unrenderable(unavailable) asset thumbnails.
- Make /asset redirect to a local asset if one exists for a Roblox asset ID when the the requested asset isnt already locally on the website.
- Added Roblox's default thumbnails to grid storage folder.
- Temporary clientsettings hack.
This commit is contained in:
Graphictoria 2023-01-05 22:18:47 -05:00
parent ffe0c2f77c
commit 241f2deb16
35 changed files with 351 additions and 24 deletions

BIN
etc/art/deleted.pdn Normal file

Binary file not shown.

BIN
etc/art/pending.pdn Normal file

Binary file not shown.

BIN
etc/art/unavailable.pdn Normal file

Binary file not shown.

View File

@ -7,6 +7,8 @@ MIX_APP_URL=http://virtubrick.net
IS_TEST_ENVIRONMENT=true
ROBLOX_COOKIE=
GAMESERVER_IP=127.0.0.1
THUMBNAILER_IP=127.0.0.1

View File

@ -0,0 +1,111 @@
<?php
/*
XlXi 2022
Asset Helper
*/
namespace App\Helpers;
use GuzzleHttp\Cookie\CookieJar;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use App\Helpers\CdnHelper;
use App\Models\Asset;
use App\Models\AssetVersion;
use App\Models\RobloxAsset;
class AssetHelper
{
private static function cookiedRequest()
{
$cookieJar = CookieJar::fromArray([
'.ROBLOXSECURITY' => env('app.robloxcookie')
], '.roblox.com');
return Http::withOptions(['cookies' => $cookieJar, 'headers' => ['User-Agent' => 'Roblox/WinInet']]);
}
public static function uploadRobloxAsset($id, $uploadToHolder = false)
{
{
$uploadedAsset = RobloxAsset::where('robloxAssetId', $id)->first();
if($uploadedAsset)
return $uploadedAsset->asset;
}
$marketplaceResult = self::cookiedRequest()->get('https://api.roblox.com/marketplace/productinfo?assetId=' . $id);
$assetResult = self::cookiedRequest()->get('https://assetdelivery.roblox.com/v2/asset?id=' . $id);
if(!$marketplaceResult->ok() || !$assetResult->ok())
return false;
$assetContent = Http::get($assetResult['locations'][0]['location']);
$hash = CdnHelper::SaveContent($assetContent->body(), $assetContent->header('Content-Type'));
$asset = Asset::create([
'creatorId' => ($uploadToHolder ? 1 : Auth::user()->id),
'name' => $marketplaceResult['Name'],
'description' => $marketplaceResult['Description'],
'approved' => true,
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
'onSale' => $marketplaceResult['IsForSale'],
'assetTypeId' => $marketplaceResult['AssetTypeId'],
'assetVersionId' => 0
]);
$assetVersion = AssetVersion::create([
'parentAsset' => $asset->id,
'localVersion' => 1,
'contentURL' => $hash
]);
$asset->assetVersionId = $assetVersion->id;
$asset->save();
RobloxAsset::create([
'robloxAssetId' => $id,
'localAssetId' => $asset->id
]);
return $asset;
}
public static function uploadCustomRobloxAsset($id, $uploadToHolder = false, $b64Content)
{
{
$uploadedAsset = RobloxAsset::where('robloxAssetId', $id)->first();
if($uploadedAsset)
return $uploadedAsset->asset;
}
$marketplaceResult = self::cookiedRequest()->get('https://api.roblox.com/marketplace/productinfo?assetId=' . $id);
if(!$marketplaceResult->ok())
return false;
$hash = CdnHelper::SaveContentB64($b64Content, 'application/octet-stream');
$asset = Asset::create([
'creatorId' => ($uploadToHolder ? 1 : Auth::user()->id),
'name' => $marketplaceResult['Name'],
'description' => $marketplaceResult['Description'],
'approved' => true,
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
'onSale' => $marketplaceResult['IsForSale'],
'assetTypeId' => $marketplaceResult['AssetTypeId'],
'assetVersionId' => 0
]);
$assetVersion = AssetVersion::create([
'parentAsset' => $asset->id,
'localVersion' => 1,
'contentURL' => $hash
]);
$asset->assetVersionId = $assetVersion->id;
$asset->save();
RobloxAsset::create([
'robloxAssetId' => $id,
'localAssetId' => $asset->id
]);
return $asset;
}
}

View File

@ -113,6 +113,29 @@ class GridHelper
return $job;
}
private static function getThumbDisk()
{
return Storage::build([
'driver' => 'local',
'root' => storage_path('app/grid/thumbnails'),
]);
}
public static function getDefaultThumbnail($fileName)
{
$disk = $self::getThumbDisk();
if(!$disk->exists($fileName))
throw new Exception('Unable to locate template file.');
return $disk->get($fileName);
}
public static function getUnknownThumbnail()
{
return $self::getDefaultThumbnail('UnknownThumbnail.png');
}
public static function getArbiter($name)
{
$query = DynamicWebConfiguration::where('name', sprintf('%sArbiterIP', $name))->first();

View File

@ -5,6 +5,8 @@ namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use App\Helpers\AssetHelper;
use App\Helpers\GridHelper;
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Jobs\AppDeployment;
@ -135,4 +137,47 @@ class AdminController extends Controller
AppDeployment::dispatch($deployment);
}
// RCC Only
function uploadRobloxAsset(Request $request)
{
$validator = Validator::make($request->all(), [
'contentId' => ['required', 'int']
]);
if($validator->fails())
return ValidationHelper::generateValidatorError($validator);
if(!GridHelper::hasAllAccess())
{
$validator->errors()->add('contentId', 'This API can only be called by the web service.');
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$asset = AssetHelper::uploadRobloxAsset($valid['contentId'], true);
return route('client.asset', ['id' => $asset->id]);
}
function uploadAsset(Request $request)
{
$validator = Validator::make($request->all(), [
'contentId' => ['required', 'int']
]);
if($validator->fails())
return ValidationHelper::generateValidatorError($validator);
if(!GridHelper::hasAllAccess())
{
$validator->errors()->add('contentId', 'This API can only be called by the web service.');
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$asset = AssetHelper::uploadCustomRobloxAsset($valid['contentId'], true, base64_encode($request->getContent()));
return route('client.asset', ['id' => $asset->id]);
}
}

View File

@ -7,6 +7,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use App\Helpers\GridHelper;
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Jobs\ArbiterRender;
@ -21,8 +22,7 @@ class ThumbnailController extends Controller
'id' => [
'required',
Rule::exists('App\Models\Asset', 'id')->where(function($query) {
return $query->where('moderated', false)
->where('approved', true);
return $query->where('moderated', false);
})
],
'type' => 'regex:/(3D|2D)/i'
@ -60,15 +60,19 @@ class ThumbnailController extends Controller
$valid['position'] = strtolower($valid['position']);
} elseif($renderType == 'Asset') {
if($model->moderated)
return response(['status' => 'success', 'data' => '/thumbs/DeletedThumbnail.png']);
if(!$model->approved)
return response(['status' => 'success', 'data' => '/thumbs/PendingThumbnail.png']);
if(!$model->assetType->renderable)
return response(['status' => 'success', 'data' => '/thumbs/UnavailableThumbnail.png']);
if(!$model->{$valid['type'] == '3d' ? 'canRender3D' : 'isRenderable'}()) {
$validator->errors()->add('id', 'This asset cannot be rendered.');
return ValidationHelper::generateValidatorError($validator);
}
// TODO: XlXi: Turn this into a switch case and fill in the rest of the unrenderables.
// Things like HTML assets should just have a generic "default" image.
//if($model->assetTypeId == 1)
// $model = Asset::where('id', $model->parentAsset)->first();
}

View File

@ -12,6 +12,7 @@ use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetVersion;
use App\Models\RobloxAsset;
class ClientController extends Controller
{
@ -56,7 +57,13 @@ class ClientController extends Controller
$validator = Validator::make($reqData, $this->{$validatorRuleSet}());
if($validator->fails())
return ValidationHelper::generateValidatorError($validator);
{
$rbxAsset = RobloxAsset::where('robloxAssetId', $request->get('id'))->first();
if($rbxAsset)
return redirect()->route('client.asset', ['id' => $rbxAsset->localAssetId]);
return redirect('https://assetdelivery.roblox.com/v1/asset?id=' . ($request->get('id') ?: 0));//return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$asset = null;

View File

@ -90,6 +90,7 @@ class ArbiterRender implements ShouldQueue
420*4, // Height // XlXi: These get scaled down by 4.
url('/') . '/'
];
switch($this->type) {
case 'Head':
case 'Shirt':

View File

@ -9,6 +9,17 @@ class AssetVersion extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'parentAsset',
'localVersion',
'contentURL'
];
public function asset()
{
return $this->belongsTo(Asset::class, 'parentAsset');

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RobloxAsset extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'localAssetId',
'robloxAssetId'
];
public function asset()
{
return $this->belongsTo(Asset::class, 'localAssetId');
}
}

View File

@ -32,6 +32,26 @@ class Asset extends Model
'updated_at' => 'datetime',
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'creatorId',
'name',
'description',
'parentAssetId',
'approved',
'priceInTokens',
'onSale',
'assetTypeId',
'assetVersionId',
'universeId',
'maxPlayers',
'chatStyleEnum'
];
/* TODO: XlXi: move to db */
protected $assetGenres = [
/* 0 */ 'All',
@ -121,11 +141,6 @@ class Asset extends Model
{
$renderId = $this->id;
// TODO: XlXi: Turn this into a switch case and fill in the rest of the unrenderables.
// Things like HTML assets should just have a generic "default" image.
//if($this->assetTypeId == 1) // Image
// $renderId = $this->parentAsset->id;
$thumbnail = Http::get(route('thumbnails.v1.asset', ['id' => $renderId, 'type' => '2d']));
if($thumbnail->json('status') == 'loading')
return ($this->assetTypeId == 9 ? 'https://virtubrick.local/images/busy/game.png' : 'https://virtubrick.local/images/busy/asset.png');

View File

@ -63,6 +63,14 @@ class RouteServiceProvider extends ServiceProvider
->middleware('api')
->namespace('App\Http\Controllers\Api')
->group(base_path('routes/api.php'));
//
// Domain: clientsettings.api.virtubrick.net
//
Route::domain('clientsettings.api.' . DomainHelper::TopLevelDomain())
->middleware('api')
->namespace('App\Http\Controllers\ClientSettings')
->group(base_path('routes/clientsettings.php'));
//
// Domain: cdn.virtubrick.net

View File

@ -224,4 +224,16 @@ return [
*/
'testenv' => (bool) env('IS_TEST_ENVIRONMENT', true),
/*
|--------------------------------------------------------------------------
| Roblox Cookie
|--------------------------------------------------------------------------
|
| Allows the site to access marketplace service APIs without rate
| limiting.
|
*/
'robloxcookie' => env('ROBLOX_COOKIE', ''),
];

View File

@ -18,7 +18,7 @@ return new class extends Migration
$table->unsignedBigInteger('creatorId');
$table->string('name');
$table->string('description')->nullable();
$table->longText('description')->nullable();
$table->unsignedBigInteger('parentAssetId')->nullable()->comment('Used by things like images that were created because of something else.');
$table->boolean('approved')->default(false);

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('roblox_assets', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('localAssetId');
$table->unsignedBigInteger('robloxAssetId');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('roblox_assets');
}
};

View File

@ -24,7 +24,6 @@ class AssetTypeSeeder extends Seeder
[
'name' => 'T-Shirt',
'renderable' => true,
'renderable3d' => true,
'copyable' => true,
'sellable' => true,
'wearable' => true
@ -108,8 +107,6 @@ class AssetTypeSeeder extends Seeder
],
[
'name' => 'Face',
'renderable' => true,
'renderable3d' => true,
'copyable' => true,
'sellable' => true,
'wearable' => true
@ -235,8 +232,7 @@ class AssetTypeSeeder extends Seeder
[
'name' => 'MeshPart',
'renderable' => true,
'renderable3d' => true,
'locked' => true
'renderable3d' => true
],
[
'name' => 'Hair Accessory',

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -228,7 +228,7 @@ class ShopItemCard extends Component {
className='img-fluid'
/>
<div className="p-2">
<p>{ item.Name }</p>
<p className="text-truncate">{ item.Name }</p>
{ item.OnSale ?
<p className="virtubrick-tokens text-truncate">{commaSeparate(item.Price)}</p>
: <p className="text-muted">Offsale</p>

View File

@ -6,8 +6,18 @@
<div class="container-md">
<h4>Auto Uploader</h4>
<div class="card p-3">
<p>todo</p>
<p>Under Construction</p>
<label for="vb-rbx-asset" class="form-label">Roblox Asset ID</label>
<div class="input-group mb-3">
<input
type="text"
class="form-control"
placeholder="Roblox Asset ID Here"
aria-label="Roblox Asset ID Here"
name="rbx-asset" id="vb-rbx-asset"
aria-describedby="vb-rbx-asset"
>
<button type="submit" class="btn btn-primary" type="button" name="rbx-asset-button" id="vb-rbx-asset-btn">Search</button>
</div>
</div>
</div>
@endpush

View File

@ -126,7 +126,7 @@
</div>
<div class="col-9">
@if ( $asset->description )
<p>{{ $asset->description }}</p>
<p>{!! nl2br(e($asset->description)) !!}</p>
@else
<p class="text-muted">This item has no description.</p>
@endif

View File

@ -32,6 +32,10 @@ Route::middleware('auth')->group(function () {
Route::get('/deploy', 'AdminController@deploy')->name('deploy');
Route::post('/deploy/{version}', 'AdminController@deployVersion')->name('deploy');
});
// RCC Only
Route::get('/upload-rbx-asset', 'AdminController@uploadRobloxAsset')->withoutMiddleware('auth')->name('uploadrbxasset');
Route::post('/upload-asset', 'AdminController@uploadAsset')->withoutMiddleware('auth')->name('uploadAsset');
});
});
});

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@ local Lighting = game:GetService("Lighting")
Lighting.ClockTime = 13
Lighting.GeographicLatitude = -5
game:Load(assetUrl)
local accoutrement = game:GetObjects(assetUrl)[1]
accoutrement.Parent = workspace
return game:GetService("ThumbnailGenerator"):Click(fileExtension, x, y, --[[hideSky = ]] true, --[[crop = ]] true)

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB