diff --git a/etc/art/deleted.pdn b/etc/art/deleted.pdn new file mode 100644 index 0000000..8cd2ccb Binary files /dev/null and b/etc/art/deleted.pdn differ diff --git a/etc/art/pending.pdn b/etc/art/pending.pdn new file mode 100644 index 0000000..27403f7 Binary files /dev/null and b/etc/art/pending.pdn differ diff --git a/etc/art/unavailable.pdn b/etc/art/unavailable.pdn new file mode 100644 index 0000000..81bb6e2 Binary files /dev/null and b/etc/art/unavailable.pdn differ diff --git a/web/.env.example b/web/.env.example index fe2832f..bb00ac3 100644 --- a/web/.env.example +++ b/web/.env.example @@ -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 diff --git a/web/app/Helpers/AssetHelper.php b/web/app/Helpers/AssetHelper.php new file mode 100644 index 0000000..559fabd --- /dev/null +++ b/web/app/Helpers/AssetHelper.php @@ -0,0 +1,111 @@ + 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; + } +} diff --git a/web/app/Helpers/GridHelper.php b/web/app/Helpers/GridHelper.php index d444866..5b15978 100644 --- a/web/app/Helpers/GridHelper.php +++ b/web/app/Helpers/GridHelper.php @@ -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(); diff --git a/web/app/Http/Controllers/Api/AdminController.php b/web/app/Http/Controllers/Api/AdminController.php index bc43d8d..ccce65e 100644 --- a/web/app/Http/Controllers/Api/AdminController.php +++ b/web/app/Http/Controllers/Api/AdminController.php @@ -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]); + } } diff --git a/web/app/Http/Controllers/Api/ThumbnailController.php b/web/app/Http/Controllers/Api/ThumbnailController.php index d215ddf..92e3949 100644 --- a/web/app/Http/Controllers/Api/ThumbnailController.php +++ b/web/app/Http/Controllers/Api/ThumbnailController.php @@ -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(); } diff --git a/web/app/Http/Controllers/Web/ClientController.php b/web/app/Http/Controllers/Web/ClientController.php index 3d61f64..34a35a9 100644 --- a/web/app/Http/Controllers/Web/ClientController.php +++ b/web/app/Http/Controllers/Web/ClientController.php @@ -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; diff --git a/web/app/Jobs/ArbiterRender.php b/web/app/Jobs/ArbiterRender.php index ce415fb..c92221e 100644 --- a/web/app/Jobs/ArbiterRender.php +++ b/web/app/Jobs/ArbiterRender.php @@ -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': diff --git a/web/app/Models/AssetVersion.php b/web/app/Models/AssetVersion.php index 3e23fa1..f78262d 100644 --- a/web/app/Models/AssetVersion.php +++ b/web/app/Models/AssetVersion.php @@ -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'); diff --git a/web/app/Models/RobloxAsset.php b/web/app/Models/RobloxAsset.php new file mode 100644 index 0000000..5b472ba --- /dev/null +++ b/web/app/Models/RobloxAsset.php @@ -0,0 +1,26 @@ +belongsTo(Asset::class, 'localAssetId'); + } +} diff --git a/web/app/Models/asset.php b/web/app/Models/asset.php index 84e0707..b04cd62 100644 --- a/web/app/Models/asset.php +++ b/web/app/Models/asset.php @@ -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'); diff --git a/web/app/Providers/RouteServiceProvider.php b/web/app/Providers/RouteServiceProvider.php index 193e149..4bd701a 100644 --- a/web/app/Providers/RouteServiceProvider.php +++ b/web/app/Providers/RouteServiceProvider.php @@ -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 diff --git a/web/config/app.php b/web/config/app.php index 48f6473..2da6cdc 100644 --- a/web/config/app.php +++ b/web/config/app.php @@ -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', ''), ]; diff --git a/web/database/migrations/2022_06_05_140351_create_assets_table.php b/web/database/migrations/2022_06_05_140351_create_assets_table.php index fa004ac..2fcc40d 100644 --- a/web/database/migrations/2022_06_05_140351_create_assets_table.php +++ b/web/database/migrations/2022_06_05_140351_create_assets_table.php @@ -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); diff --git a/web/database/migrations/2023_01_04_184927_create_roblox_assets_table.php b/web/database/migrations/2023_01_04_184927_create_roblox_assets_table.php new file mode 100644 index 0000000..38e42e8 --- /dev/null +++ b/web/database/migrations/2023_01_04_184927_create_roblox_assets_table.php @@ -0,0 +1,35 @@ +id(); + + $table->unsignedBigInteger('localAssetId'); + $table->unsignedBigInteger('robloxAssetId'); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('roblox_assets'); + } +}; diff --git a/web/database/seeders/AssetTypeSeeder.php b/web/database/seeders/AssetTypeSeeder.php index 3302e4c..4fa89d0 100644 --- a/web/database/seeders/AssetTypeSeeder.php +++ b/web/database/seeders/AssetTypeSeeder.php @@ -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', diff --git a/web/public/thumbs/DeletedThumbnail.png b/web/public/thumbs/DeletedThumbnail.png new file mode 100644 index 0000000..ffbe366 Binary files /dev/null and b/web/public/thumbs/DeletedThumbnail.png differ diff --git a/web/public/thumbs/PendingThumbnail.png b/web/public/thumbs/PendingThumbnail.png new file mode 100644 index 0000000..88cc0b0 Binary files /dev/null and b/web/public/thumbs/PendingThumbnail.png differ diff --git a/web/public/thumbs/UnavailableThumbnail.png b/web/public/thumbs/UnavailableThumbnail.png new file mode 100644 index 0000000..4d64513 Binary files /dev/null and b/web/public/thumbs/UnavailableThumbnail.png differ diff --git a/web/resources/js/components/Shop.js b/web/resources/js/components/Shop.js index 2195e2b..1b3aca9 100644 --- a/web/resources/js/components/Shop.js +++ b/web/resources/js/components/Shop.js @@ -228,7 +228,7 @@ class ShopItemCard extends Component { className='img-fluid' />
{ item.Name }
+{ item.Name }
{ item.OnSale ?{commaSeparate(item.Price)}
:Offsale
diff --git a/web/resources/views/web/admin/autoupload.blade.php b/web/resources/views/web/admin/autoupload.blade.php index 712d41e..cecf564 100644 --- a/web/resources/views/web/admin/autoupload.blade.php +++ b/web/resources/views/web/admin/autoupload.blade.php @@ -6,8 +6,18 @@todo
-Under Construction
+ +{{ $asset->description }}
+{!! nl2br(e($asset->description)) !!}
@elseThis item has no description.
@endif diff --git a/web/routes/api.php b/web/routes/api.php index f53117e..06ae448 100644 --- a/web/routes/api.php +++ b/web/routes/api.php @@ -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'); }); }); }); diff --git a/web/routes/clientsettings.php b/web/routes/clientsettings.php new file mode 100644 index 0000000..0085672 --- /dev/null +++ b/web/routes/clientsettings.php @@ -0,0 +1,16 @@ +header('Content-Type', 'application/json'); +}); + +Route::get('/Setting/QuietGet/RccGames', function(){ + return response("{\n}") + ->header('Content-Type', 'application/json'); +}); + +Route::fallback(function () { + return response('404 not found.', 404) + ->header('Content-Type', 'text/plain'); +}); \ No newline at end of file diff --git a/web/storage/app/grid/scripts/Hat.lua b/web/storage/app/grid/scripts/Hat.lua index 636545d..bc8fa01 100644 --- a/web/storage/app/grid/scripts/Hat.lua +++ b/web/storage/app/grid/scripts/Hat.lua @@ -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) diff --git a/web/storage/app/grid/thumbnails/AudioThumbnail.png b/web/storage/app/grid/thumbnails/AudioThumbnail.png new file mode 100644 index 0000000..9583590 Binary files /dev/null and b/web/storage/app/grid/thumbnails/AudioThumbnail.png differ diff --git a/web/storage/app/grid/thumbnails/BlankThumbnail.gif b/web/storage/app/grid/thumbnails/BlankThumbnail.gif new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/web/storage/app/grid/thumbnails/BlankThumbnail.gif differ diff --git a/web/storage/app/grid/thumbnails/BrokenThumbnail.jpg b/web/storage/app/grid/thumbnails/BrokenThumbnail.jpg new file mode 100644 index 0000000..67a0a9f Binary files /dev/null and b/web/storage/app/grid/thumbnails/BrokenThumbnail.jpg differ diff --git a/web/storage/app/grid/thumbnails/ReviewPendingThumbnail.png b/web/storage/app/grid/thumbnails/ReviewPendingThumbnail.png new file mode 100644 index 0000000..49c8c7e Binary files /dev/null and b/web/storage/app/grid/thumbnails/ReviewPendingThumbnail.png differ diff --git a/web/storage/app/grid/thumbnails/TeeShirtThumbnail.png b/web/storage/app/grid/thumbnails/TeeShirtThumbnail.png new file mode 100644 index 0000000..32eefc0 Binary files /dev/null and b/web/storage/app/grid/thumbnails/TeeShirtThumbnail.png differ diff --git a/web/storage/app/grid/thumbnails/UnapprovedThumbnail.png b/web/storage/app/grid/thumbnails/UnapprovedThumbnail.png new file mode 100644 index 0000000..3db9bcb Binary files /dev/null and b/web/storage/app/grid/thumbnails/UnapprovedThumbnail.png differ diff --git a/web/storage/app/grid/thumbnails/UnavailableThumbnail.jpg b/web/storage/app/grid/thumbnails/UnavailableThumbnail.jpg new file mode 100644 index 0000000..b948670 Binary files /dev/null and b/web/storage/app/grid/thumbnails/UnavailableThumbnail.jpg differ diff --git a/web/storage/app/grid/thumbnails/UnknownThumbnail.png b/web/storage/app/grid/thumbnails/UnknownThumbnail.png new file mode 100644 index 0000000..1a72174 Binary files /dev/null and b/web/storage/app/grid/thumbnails/UnknownThumbnail.png differ