the browser-facing portion of osu!
at master 13 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Http\Controllers; 7 8use App\Exceptions\Handler as ExceptionsHandler; 9use App\Jobs\BeatmapsetDelete; 10use App\Libraries\BeatmapsetDiscussion\Review; 11use App\Libraries\CommentBundle; 12use App\Libraries\Search\BeatmapsetSearchCached; 13use App\Libraries\Search\BeatmapsetSearchRequestParams; 14use App\Models\Beatmap; 15use App\Models\BeatmapDownload; 16use App\Models\BeatmapMirror; 17use App\Models\Beatmapset; 18use App\Models\BeatmapsetEvent; 19use App\Models\BeatmapsetWatch; 20use App\Models\Genre; 21use App\Models\Language; 22use App\Transformers\BeatmapsetTransformer; 23use Auth; 24use Carbon\Carbon; 25use DB; 26use Request; 27 28/** 29 * @group Beatmapsets 30 */ 31class BeatmapsetsController extends Controller 32{ 33 public function __construct() 34 { 35 parent::__construct(); 36 37 $this->middleware('require-scopes:public', ['only' => ['lookup', 'search', 'show']]); 38 } 39 40 public function destroy($id) 41 { 42 $beatmapset = Beatmapset::findOrFail($id); 43 44 priv_check('BeatmapsetDelete', $beatmapset)->ensureCan(); 45 46 (new BeatmapsetDelete($beatmapset, Auth::user()))->handle(); 47 } 48 49 public function index() 50 { 51 $canAdvancedSearch = priv_check('BeatmapsetAdvancedSearch')->can(); 52 // only cache if guest user and guest advanced search is disabled 53 $beatmapsetsJsonString = !Auth::check() && !$canAdvancedSearch 54 ? cache_remember_mutexed( 55 'beatmapsets_guest_str', 56 600, 57 '{}', 58 fn () => json_encode($this->getSearchResponse([])['content']) 59 ) : json_encode($this->getSearchResponse()['content']); 60 61 return ext_view('beatmapsets.index', [ 62 'beatmapsetsJsonString' => $beatmapsetsJsonString, 63 'canAdvancedSearch' => $canAdvancedSearch, 64 ]); 65 } 66 67 public function lookup() 68 { 69 $beatmapId = get_int(request('beatmap_id')); 70 71 if ($beatmapId === null) { 72 abort(404); 73 } 74 75 $beatmap = Beatmap::findOrFail($beatmapId); 76 77 return $this->show($beatmap->beatmapset_id); 78 } 79 80 public function show($id) 81 { 82 $beatmapset = ( 83 priv_check('BeatmapsetShowDeleted')->can() 84 ? Beatmapset::withTrashed()->whereHas('allBeatmaps') 85 : Beatmapset::whereHas('beatmaps') 86 )->findOrFail($id); 87 88 $set = $this->showJson($beatmapset); 89 90 if (is_api_request()) { 91 return $set; 92 } else { 93 $commentBundle = CommentBundle::forEmbed($beatmapset); 94 95 if (priv_check('BeatmapsetMetadataEdit', $beatmapset)->can()) { 96 $genres = Genre::listing(); 97 $languages = Language::listing(); 98 } else { 99 $genres = []; 100 $languages = []; 101 } 102 103 $noindex = !$beatmapset->esShouldIndex(); 104 105 set_opengraph($beatmapset); 106 107 return ext_view('beatmapsets.show', compact( 108 'beatmapset', 109 'commentBundle', 110 'genres', 111 'languages', 112 'noindex', 113 'set' 114 )); 115 } 116 } 117 118 /** 119 * TODO: documentation 120 * 121 * @usesCursor 122 */ 123 public function search() 124 { 125 $response = $this->getSearchResponse(); 126 127 return response($response['content'], $response['status']); 128 } 129 130 public function discussion($id) 131 { 132 $beatmapset = Beatmapset::findOrFail($id); 133 134 $initialData = [ 135 'beatmapset' => $beatmapset->defaultDiscussionJson(), 136 'reviews_config' => Review::config(), 137 ]; 138 139 BeatmapsetWatch::markRead($beatmapset, Auth::user()); 140 141 if (is_json_request()) { 142 return $initialData; 143 } else { 144 return ext_view('beatmapsets.discussion', compact('beatmapset', 'initialData')); 145 } 146 } 147 148 public function discussionLastUpdate($id) 149 { 150 return response(['last_update' => Beatmapset::findOrFail($id)->lastDiscussionTime()]); 151 } 152 153 public function discussionUnlock($id) 154 { 155 priv_check('BeatmapsetDiscussionLock')->ensureCan(); 156 157 $beatmapset = Beatmapset::findOrFail($id); 158 $beatmapset->discussionUnlock(Auth::user()); 159 160 return $beatmapset->defaultDiscussionJson(); 161 } 162 163 public function discussionLock($id) 164 { 165 priv_check('BeatmapsetDiscussionLock')->ensureCan(); 166 167 $beatmapset = Beatmapset::findOrFail($id); 168 $beatmapset->discussionLock(Auth::user(), request('reason')); 169 170 return $beatmapset->defaultDiscussionJson(); 171 } 172 173 public function download($id) 174 { 175 if (!is_api_request() && !from_app_url()) { 176 return ujs_redirect(route('beatmapsets.show', ['beatmapset' => rawurlencode($id)])); 177 } 178 179 $beatmapset = Beatmapset::findOrFail($id); 180 181 if ($beatmapset->download_disabled) { 182 abort(404); 183 } 184 185 priv_check('BeatmapsetDownload', $beatmapset)->ensureCan(); 186 187 $user = Auth::user(); 188 $userId = $user->getKey(); 189 $recentlyDownloaded = BeatmapDownload::where('user_id', $userId) 190 ->where('timestamp', '>', Carbon::now()->subHours()->getTimestamp()) 191 ->count(); 192 193 if ($recentlyDownloaded > $user->beatmapsetDownloadAllowance()) { 194 abort(429, osu_trans('beatmapsets.download.limit_exceeded')); 195 } 196 197 $noVideo = get_bool(Request::input('noVideo', false)); 198 $mirror = BeatmapMirror::getRandomForRegion(request_country()) 199 ?? BeatmapMirror::getDefault() 200 ?? abort(503, osu_trans('beatmapsets.download.no_mirrors')); 201 202 BeatmapDownload::create([ 203 'user_id' => $userId, 204 'timestamp' => time(), 205 'beatmapset_id' => $beatmapset->beatmapset_id, 206 'fulfilled' => 1, 207 'mirror_id' => $mirror->mirror_id, 208 ]); 209 210 return redirect($mirror->generateURL($beatmapset, $noVideo)); 211 } 212 213 public function nominate($id) 214 { 215 $beatmapset = Beatmapset::findOrFail($id); 216 $params = get_params(request()->all(), null, ['playmodes:string[]']); 217 218 priv_check('BeatmapsetNominate', $beatmapset)->ensureCan(); 219 220 $beatmapset->nominate(Auth::user(), $params['playmodes'] ?? []); 221 222 BeatmapsetWatch::markRead($beatmapset, Auth::user()); 223 224 return $beatmapset->defaultDiscussionJson(); 225 } 226 227 public function love($id) 228 { 229 $beatmapset = Beatmapset::findOrFail($id); 230 231 $params = get_params(request()->all(), null, ['beatmap_ids:int[]'], ['null_missing' => true]); 232 233 priv_check('BeatmapsetLove')->ensureCan(); 234 235 $nomination = $beatmapset->love(Auth::user(), $params['beatmap_ids']); 236 if (!$nomination['result']) { 237 return error_popup($nomination['message']); 238 } 239 240 BeatmapsetWatch::markRead($beatmapset, Auth::user()); 241 242 return $beatmapset->defaultDiscussionJson(); 243 } 244 245 public function removeFromLoved($id) 246 { 247 $beatmapset = Beatmapset::findOrFail($id); 248 249 priv_check('BeatmapsetRemoveFromLoved')->ensureCan(); 250 251 $result = $beatmapset->removeFromLoved(Auth::user(), request('reason')); 252 if (!$result['result']) { 253 return error_popup($result['message']); 254 } 255 256 BeatmapsetWatch::markRead($beatmapset, Auth::user()); 257 258 return $beatmapset->defaultDiscussionJson(); 259 } 260 261 public function update($id) 262 { 263 $beatmapset = Beatmapset::findOrFail($id); 264 $params = request()->all(); 265 266 if (isset($params['description']) && is_string($params['description'])) { 267 priv_check('BeatmapsetDescriptionEdit', $beatmapset)->ensureCan(); 268 269 $description = $params['description']; 270 271 if (!$beatmapset->updateDescription($description, Auth::user())) { 272 abort(422, 'failed updating description'); 273 } 274 275 $beatmapset->refresh(); 276 } 277 278 $metadataParams = get_params($params, 'beatmapset', [ 279 'genre_id:int', 280 'language_id:int', 281 'nsfw:bool', 282 ]); 283 284 if (count($metadataParams) > 0) { 285 priv_check('BeatmapsetMetadataEdit', $beatmapset)->ensureCan(); 286 } 287 288 $updateParams = [ 289 ...$metadataParams, 290 ...get_params($params, 'beatmapset', [ 291 'offset:int', 292 'tags:string', 293 ]), 294 ]; 295 296 if (array_key_exists('offset', $updateParams)) { 297 priv_check('BeatmapsetOffsetEdit')->ensureCan(); 298 } 299 300 if (array_key_exists('tags', $updateParams)) { 301 priv_check('BeatmapsetTagsEdit')->ensureCan(); 302 } 303 304 if (count($updateParams) > 0) { 305 DB::transaction(function () use ($beatmapset, $updateParams) { 306 $oldGenreId = $beatmapset->genre_id; 307 $oldLanguageId = $beatmapset->language_id; 308 $oldNsfw = $beatmapset->nsfw; 309 $oldOffset = $beatmapset->offset; 310 $oldTags = $beatmapset->tags; 311 $user = auth()->user(); 312 313 $beatmapset->fill($updateParams)->saveOrExplode(); 314 315 if ($oldGenreId !== $beatmapset->genre_id) { 316 BeatmapsetEvent::log(BeatmapsetEvent::GENRE_EDIT, $user, $beatmapset, [ 317 'old' => Genre::find($oldGenreId)->name, 318 'new' => $beatmapset->genre->name, 319 ])->saveOrExplode(); 320 } 321 322 if ($oldLanguageId !== $beatmapset->language_id) { 323 BeatmapsetEvent::log(BeatmapsetEvent::LANGUAGE_EDIT, $user, $beatmapset, [ 324 'old' => Language::find($oldLanguageId)->name, 325 'new' => $beatmapset->language->name, 326 ])->saveOrExplode(); 327 } 328 329 if ($oldNsfw !== $beatmapset->nsfw) { 330 BeatmapsetEvent::log(BeatmapsetEvent::NSFW_TOGGLE, $user, $beatmapset, [ 331 'old' => $oldNsfw, 332 'new' => $beatmapset->nsfw, 333 ])->saveOrExplode(); 334 } 335 336 if ($oldOffset !== $beatmapset->offset) { 337 BeatmapsetEvent::log(BeatmapsetEvent::OFFSET_EDIT, $user, $beatmapset, [ 338 'old' => $oldOffset, 339 'new' => $beatmapset->offset, 340 ])->saveOrExplode(); 341 } 342 343 if ($oldTags !== $beatmapset->tags) { 344 BeatmapsetEvent::log(BeatmapsetEvent::TAGS_EDIT, $user, $beatmapset, [ 345 'old' => $oldTags, 346 'new' => $beatmapset->tags, 347 ])->saveOrExplode(); 348 } 349 }); 350 } 351 352 return $this->showJson($beatmapset); 353 } 354 355 private function getSearchResponse(?array $params = null) 356 { 357 $params = new BeatmapsetSearchRequestParams($params ?? request()->all(), auth()->user()); 358 $search = (new BeatmapsetSearchCached($params)); 359 360 $records = datadog_timing(function () use ($search) { 361 return $search->records(); 362 }, $GLOBALS['cfg']['datadog-helper']['prefix_web'].'.search', ['type' => 'beatmapset']); 363 364 $error = $search->getError(); 365 366 return [ 367 'content' => array_merge([ 368 'beatmapsets' => json_collection( 369 $records, 370 new BeatmapsetTransformer(), 371 ['beatmaps.max_combo', 'pack_tags'] 372 ), 373 'search' => [ 374 'sort' => $search->getParams()->getSort(), 375 ], 376 'recommended_difficulty' => $params->getRecommendedDifficulty(), 377 'error' => search_error_message($error), 378 'total' => $search->count(), 379 ], cursor_for_response($search->getSortCursor())), 380 'status' => $error === null ? 200 : ExceptionsHandler::statusCode($error), 381 ]; 382 } 383 384 private function showJson($beatmapset) 385 { 386 $beatmapRelation = $beatmapset->trashed() 387 ? 'allBeatmaps' 388 : 'beatmaps'; 389 $beatmapset->load([ 390 "{$beatmapRelation}.baseDifficultyRatings", 391 "{$beatmapRelation}.baseMaxCombo", 392 "{$beatmapRelation}.failtimes", 393 "{$beatmapRelation}.beatmapOwners.user", 394 'genre', 395 'language', 396 'user', 397 ]); 398 399 $transformer = new BeatmapsetTransformer(); 400 $transformer->relatedUsersType = 'show'; 401 402 return json_item($beatmapset, $transformer, [ 403 'beatmaps', 404 'beatmaps.failtimes', 405 'beatmaps.max_combo', 406 'beatmaps.owners', 407 'beatmaps.top_tag_ids', 408 'converts', 409 'converts.failtimes', 410 'converts.owners', 411 'current_nominations', 412 'current_user_attributes', 413 'description', 414 'genre', 415 'language', 416 'pack_tags', 417 'ratings', 418 'recent_favourites', 419 'related_tags', 420 'related_users', 421 'user', 422 ]); 423 } 424}