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}