the browser-facing portion of osu!
at master 327 lines 12 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\Transformers; 7 8use App\Libraries\Beatmapset\NominateBeatmapset; 9use App\Models\Beatmap; 10use App\Models\BeatmapDiscussion; 11use App\Models\Beatmapset; 12use App\Models\BeatmapsetEvent; 13use App\Models\BeatmapsetWatch; 14use App\Models\DeletedUser; 15use App\Models\User; 16use Auth; 17use Ds\Set; 18use Illuminate\Database\Eloquent\Collection as EloquentCollection; 19use League\Fractal; 20use League\Fractal\Resource\Collection; 21 22class BeatmapsetCompactTransformer extends TransformerAbstract 23{ 24 protected array $availableIncludes = [ 25 'availability', 26 'beatmaps', 27 'converts', 28 'current_nominations', 29 'current_user_attributes', 30 'description', 31 'discussions', 32 'eligible_main_rulesets', 33 'events', 34 'genre', 35 'has_favourited', 36 'language', 37 'pack_tags', 38 'main_ruleset', 39 'nominations', 40 'ratings', 41 'recent_favourites', 42 'related_users', 43 'related_tags', 44 'user', 45 ]; 46 47 // TODO: switch to enum after php 8.1 48 public string $relatedUsersType = 'discussions'; 49 50 protected $beatmapTransformer = BeatmapCompactTransformer::class; 51 52 protected $permissions = [ 53 'current_user_attributes' => 'IsNotOAuth', 54 'has_favourited' => 'IsSpecialScope', // TODO: make a scope for this. 55 ]; 56 57 public function transform(Beatmapset $beatmapset) 58 { 59 return [ 60 'artist' => $beatmapset->artist, 61 'artist_unicode' => $beatmapset->artist_unicode, 62 'covers' => $beatmapset->allCoverURLs(), 63 'creator' => $beatmapset->creator, 64 'favourite_count' => $beatmapset->favourite_count, 65 'hype' => $beatmapset->canBeHyped() ? [ 66 'current' => $beatmapset->hype, 67 'required' => $beatmapset->requiredHype(), 68 ] : null, 69 'id' => $beatmapset->beatmapset_id, 70 'nsfw' => $beatmapset->nsfw, 71 'offset' => $beatmapset->offset, 72 'play_count' => $beatmapset->play_count, 73 'preview_url' => $beatmapset->previewURL(), 74 'source' => $beatmapset->source, 75 'spotlight' => $beatmapset->spotlight, 76 'status' => $beatmapset->status(), 77 'title' => $beatmapset->title, 78 'title_unicode' => $beatmapset->title_unicode, 79 'track_id' => $beatmapset->track_id, 80 'user_id' => $beatmapset->user_id, 81 'video' => $beatmapset->video, 82 ]; 83 } 84 85 public function includeAvailability(Beatmapset $beatmapset) 86 { 87 return $this->primitive([ 88 'download_disabled' => $beatmapset->download_disabled, 89 'more_information' => $beatmapset->download_disabled_url, 90 ]); 91 } 92 93 public function includeBeatmaps(Beatmapset $beatmapset, Fractal\ParamBag $params) 94 { 95 96 return $this->collection($this->beatmaps($beatmapset, $params), new $this->beatmapTransformer()); 97 } 98 99 public function includeConverts(Beatmapset $beatmapset) 100 { 101 $converts = []; 102 103 foreach ($this->beatmaps($beatmapset) as $beatmap) { 104 if ($beatmap->mode !== 'osu') { 105 continue; 106 } 107 108 foreach (Beatmap::MODES as $modeStr => $modeInt) { 109 if ($modeStr === 'osu') { 110 continue; 111 } 112 113 $beatmap = clone $beatmap; 114 115 $beatmap->playmode = $modeInt; 116 $beatmap->convert = true; 117 118 array_push($converts, $beatmap); 119 } 120 } 121 122 return $this->collection($converts, new BeatmapTransformer()); 123 } 124 125 public function includeCurrentNominations(Beatmapset $beatmapset): Collection 126 { 127 return $this->collection($beatmapset->beatmapsetNominationsCurrent, new BeatmapsetNominationTransformer()); 128 } 129 130 public function includeCurrentUserAttributes(Beatmapset $beatmapset) 131 { 132 $currentUser = Auth::user(); 133 134 $hypeValidation = $beatmapset->validateHypeBy($currentUser); 135 136 return $this->primitive([ 137 'can_beatmap_update_owner' => priv_check('BeatmapUpdateOwner', $beatmapset)->can(), 138 'can_delete' => !$beatmapset->isScoreable() && priv_check('BeatmapsetDelete', $beatmapset)->can(), 139 'can_edit_metadata' => priv_check('BeatmapsetMetadataEdit', $beatmapset)->can(), 140 'can_edit_offset' => priv_check('BeatmapsetOffsetEdit')->can(), 141 'can_edit_tags' => priv_check('BeatmapsetTagsEdit')->can(), 142 'can_hype' => $hypeValidation['result'], 143 'can_hype_reason' => $hypeValidation['message'] ?? null, 144 'can_love' => $beatmapset->isLoveable() && priv_check('BeatmapsetLove')->can(), 145 'can_remove_from_loved' => $beatmapset->isLoved() && priv_check('BeatmapsetRemoveFromLoved')->can(), 146 'is_watching' => BeatmapsetWatch::check($beatmapset, Auth::user()), 147 'new_hype_time' => json_time($currentUser?->newHypeTime()), 148 'nomination_modes' => $currentUser?->nominationModes(), 149 'remaining_hype' => $currentUser?->remainingHype() ?? 0, 150 ]); 151 } 152 153 public function includeDescription(Beatmapset $beatmapset) 154 { 155 return $this->item($beatmapset, new BeatmapsetDescriptionTransformer()); 156 } 157 158 public function includeDiscussions(Beatmapset $beatmapset) 159 { 160 return $this->collection( 161 $beatmapset->beatmapDiscussions, 162 new BeatmapDiscussionTransformer() 163 ); 164 } 165 166 public function includeEvents(Beatmapset $beatmapset) 167 { 168 return $this->collection( 169 $beatmapset->events->all(), 170 new BeatmapsetEventTransformer() 171 ); 172 } 173 174 public function includeHasFavourited(Beatmapset $beatmapset) 175 { 176 return $this->primitive(auth()->user()->hasFavourited($beatmapset)); 177 } 178 179 public function includeGenre(Beatmapset $beatmapset) 180 { 181 return $this->item($beatmapset->genre, new GenreTransformer()); 182 } 183 184 public function includeLanguage(Beatmapset $beatmapset) 185 { 186 return $this->item($beatmapset->language, new LanguageTransformer()); 187 } 188 189 public function includeEligibleMainRulesets(Beatmapset $beatmapset) 190 { 191 return $this->primitive($beatmapset->eligibleMainRulesets()); 192 } 193 194 public function includeNominations(Beatmapset $beatmapset) 195 { 196 $result = [ 197 'legacy_mode' => $beatmapset->isLegacyNominationMode(), 198 'current' => $beatmapset->currentNominationCount(), 199 'required_meta' => NominateBeatmapset::requiredNominationsConfig(), 200 ]; 201 202 if ($beatmapset->isPending()) { 203 $currentUser = Auth::user(); 204 $disqualificationEvent = $beatmapset->disqualificationEvent(); 205 $resetEvent = $beatmapset->resetEvent(); 206 207 if ($resetEvent !== null && $resetEvent->type === BeatmapsetEvent::NOMINATION_RESET) { 208 $result['nomination_reset'] = json_item($resetEvent, 'BeatmapsetEvent'); 209 } 210 if ($disqualificationEvent !== null) { 211 $result['disqualification'] = json_item($disqualificationEvent, 'BeatmapsetEvent'); 212 } 213 if ($currentUser !== null) { 214 $result['nominated'] = $beatmapset->beatmapsetNominations()->current()->where('user_id', $currentUser->getKey())->exists(); 215 } 216 } elseif ($beatmapset->isQualified()) { 217 $queueStatus = $beatmapset->rankingQueueStatus(); 218 219 $result['ranking_eta'] = json_time($queueStatus['eta']); 220 $result['ranking_queue_position'] = $queueStatus['position']; 221 } 222 223 return $this->primitive($result); 224 } 225 226 public function includePackTags(Beatmapset $beatmapset) 227 { 228 return $this->primitive($beatmapset->pack_tags); 229 } 230 231 public function includeUser(Beatmapset $beatmapset) 232 { 233 return $this->item( 234 $beatmapset->user ?? (new DeletedUser()), 235 new UserCompactTransformer() 236 ); 237 } 238 239 public function includeRatings(Beatmapset $beatmapset) 240 { 241 return $this->primitive($beatmapset->ratingsCount()); 242 } 243 244 public function includeRecentFavourites(Beatmapset $beatmapset) 245 { 246 return $this->collection( 247 $beatmapset->recentFavourites(), 248 new UserCompactTransformer() 249 ); 250 } 251 252 public function includeRelatedUsers(Beatmapset $beatmapset) 253 { 254 $userIds = new Set([$beatmapset->user_id]); 255 switch ($this->relatedUsersType) { 256 case 'discussions': 257 $beatmaps = $beatmapset->allBeatmaps; 258 $userIds->add(...$beatmaps->pluck('user_id')); 259 $userIds->add(...$beatmaps->flatMap->beatmapOwners->pluck('user_id')); 260 261 foreach ($beatmapset->beatmapDiscussions as $discussion) { 262 if (!priv_check('BeatmapDiscussionShow', $discussion)->can()) { 263 continue; 264 } 265 266 $userIds->add($discussion->user_id); 267 $userIds->add($discussion->deleted_by_id); 268 269 foreach ($discussion->beatmapDiscussionPosts as $post) { 270 if (!priv_check('BeatmapDiscussionPostShow', $post)->can()) { 271 continue; 272 } 273 274 $userIds->add($post->user_id); 275 $userIds->add($post->last_editor_id); 276 $userIds->add($post->deleted_by_id); 277 } 278 279 foreach ($discussion->beatmapDiscussionVotes->sortByDesc('created_at')->take(BeatmapDiscussion::VOTES_TO_SHOW) as $vote) { 280 $userIds->add($vote->user_id); 281 } 282 } 283 284 foreach ($beatmapset->events as $event) { 285 if (priv_check('BeatmapsetEventViewUserId', $event)->can()) { 286 $userIds->add($event->user_id); 287 } 288 } 289 break; 290 case 'show': 291 $beatmaps = $this->beatmaps($beatmapset); 292 $userIds->add(...$beatmaps->pluck('user_id')); 293 $userIds->add(...$beatmaps->flatMap->beatmapOwners->pluck('user_id')); 294 $userIds->add(...$beatmapset->beatmapsetNominationsCurrent->pluck('user_id')); 295 break; 296 } 297 298 $users = User::with('userGroups')->whereIn('user_id', $userIds->toArray())->get(); 299 300 return $this->collection($users, new UserCompactTransformer()); 301 } 302 303 public function includeRelatedTags(Beatmapset $beatmapset) 304 { 305 $beatmaps = $this->beatmaps($beatmapset); 306 $tagIdSet = new Set($beatmaps->flatMap->topTagIds()->pluck('tag_id')); 307 308 $cachedTags = app('tags'); 309 $json = []; 310 311 foreach ($tagIdSet as $tagId) { 312 $tag = $cachedTags->get($tagId); 313 if ($tag !== null) { 314 $json[] = $tag; 315 } 316 } 317 318 return $this->primitive($json); 319 } 320 321 private function beatmaps(Beatmapset $beatmapset, ?Fractal\ParamBag $params = null): EloquentCollection 322 { 323 $rel = $beatmapset->trashed() || ($params !== null && $params->get('with_trashed')) ? 'allBeatmaps' : 'beatmaps'; 324 325 return $beatmapset->$rel; 326 } 327}