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.
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', ''),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 43 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 9.3 KiB |