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\MorphMap;
9use App\Libraries\Search\ScoreSearchParams;
10use App\Models\Beatmap;
11use App\Models\User;
12use App\Models\UserProfileCustomization;
13use Illuminate\Support\Arr;
14use League\Fractal\Resource\ResourceInterface;
15
16class UserCompactTransformer extends TransformerAbstract
17{
18 const CARD_INCLUDES = [
19 'country',
20 'cover',
21 'groups',
22 'team',
23 ];
24
25 const CARD_INCLUDES_PRELOAD = [
26 'team',
27 'userGroups',
28 ];
29
30 // Paired with static::listIncludesPreload
31 const LIST_INCLUDES = [
32 ...self::CARD_INCLUDES,
33 'statistics',
34 'support_level',
35 ];
36
37 const PROFILE_HEADER_INCLUDES = [
38 'active_tournament_banner',
39 'active_tournament_banners',
40 'badges',
41 'comments_count',
42 'follower_count',
43 'groups',
44 'mapping_follower_count',
45 'previous_usernames',
46 'support_level',
47 ];
48
49 protected string $mode;
50
51 protected array $availableIncludes = [
52 'account_history',
53 'active_tournament_banner', // deprecated
54 'active_tournament_banners',
55 'badges',
56 'beatmap_playcounts_count',
57 'blocks',
58 'comments_count',
59 'country',
60 'cover',
61 'daily_challenge_user_stats',
62 'favourite_beatmapset_count',
63 'follow_user_mapping',
64 'follower_count',
65 'friends',
66 'graveyard_beatmapset_count',
67 'groups',
68 'guest_beatmapset_count',
69 'is_admin',
70 'is_bng',
71 'is_full_bn',
72 'is_gmt',
73 'is_limited_bn',
74 'is_moderator',
75 'is_nat',
76 'is_restricted',
77 'is_silenced',
78 'kudosu',
79 'loved_beatmapset_count',
80 'mapping_follower_count',
81 'monthly_playcounts',
82 'nominated_beatmapset_count',
83 'page',
84 'pending_beatmapset_count',
85 'previous_usernames',
86 'rank_highest',
87 'ranked_beatmapset_count',
88 'replays_watched_counts',
89 'scores_best_count',
90 'scores_first_count',
91 'scores_pinned_count',
92 'scores_recent_count',
93 'session_verified',
94 'statistics',
95 'statistics_rulesets',
96 'support_level',
97 'team',
98 'unread_pm_count',
99 'user_achievements',
100 'user_preferences',
101
102 // TODO: should be alphabetically ordered but lazer relies on being after statistics.
103 'rank_history',
104 ];
105
106 protected $permissions = [
107 'friends' => 'IsNotOAuth',
108 'is_admin' => 'IsNotOAuth',
109 'is_bng' => 'IsNotOAuth',
110 'is_full_bn' => 'IsNotOAuth',
111 'is_gmt' => 'IsNotOAuth',
112 'is_limited_bn' => 'IsNotOAuth',
113 'is_moderator' => 'IsNotOAuth',
114 'is_nat' => 'IsNotOAuth',
115 'is_restricted' => 'UserShowRestrictedStatus',
116 'is_silenced' => 'IsNotOAuth',
117 ];
118
119 public static function listIncludesPreload(string $rulesetName): array
120 {
121 return [
122 ...static::CARD_INCLUDES_PRELOAD,
123 User::statisticsRelationName($rulesetName),
124 'supporterTagPurchases',
125 ];
126 }
127
128 public function transform(User $user)
129 {
130 return [
131 'avatar_url' => $user->user_avatar,
132 'country_code' => $user->country_acronym,
133 'default_group' => $user->defaultGroup()->identifier,
134 'id' => $user->user_id,
135 'is_active' => $user->isActive(),
136 'is_bot' => $user->isBot(),
137 'is_deleted' => $user->trashed(),
138 'is_online' => $user->isOnline(),
139 'is_supporter' => $user->isSupporter(),
140 'last_visit' => json_time($user->displayed_last_visit),
141 'pm_friends_only' => $user->pm_friends_only,
142 'profile_colour' => $user->user_colour,
143 'username' => $user->username,
144 ];
145 }
146
147 public function includeAccountHistory(User $user)
148 {
149 $histories = $user->accountHistories()->recent();
150
151 if (!priv_check('UserSilenceShowExtendedInfo')->can()) {
152 $histories->default();
153 } else {
154 $histories->with('actor');
155 }
156
157 return $this->collection(
158 $histories->get(),
159 new UserAccountHistoryTransformer()
160 );
161 }
162
163 public function includeActiveTournamentBanner(User $user)
164 {
165 $banner = $user->profileBannersActive->last();
166
167 return $banner === null
168 ? $this->primitive(null)
169 : $this->item($banner, new ProfileBannerTransformer());
170 }
171
172 public function includeActiveTournamentBanners(User $user)
173 {
174 return $this->collection(
175 $user->profileBannersActive,
176 new ProfileBannerTransformer(),
177 );
178 }
179
180 public function includeBadges(User $user)
181 {
182 return $this->collection(
183 $user->badges()->orderBy('awarded', 'DESC')->get(),
184 new UserBadgeTransformer()
185 );
186 }
187
188 public function includeBeatmapPlaycountsCount(User $user)
189 {
190 return $this->primitive($user->beatmapPlaycounts()->count());
191 }
192
193 public function includeBlocks(User $user)
194 {
195 return $this->collection(
196 $user->relations()->blocks()->get(),
197 new UserRelationTransformer()
198 );
199 }
200
201 public function includeCommentsCount(User $user)
202 {
203 return $this->primitive($user->comments()->withoutTrashed()->count());
204 }
205
206 public function includeCountry(User $user)
207 {
208 $countryAcronym = $user->country_acronym;
209 $country = $countryAcronym === null
210 ? null
211 : app('countries')->byCode($countryAcronym);
212
213 return $country === null
214 ? $this->primitive(null)
215 : $this->item($country, new CountryTransformer());
216 }
217
218 public function includeCover(User $user)
219 {
220 $cover = $user->cover();
221
222 return $this->primitive([
223 'custom_url' => $cover->customUrl(),
224 'url' => $cover->url(),
225 // cast to string for backward compatibility
226 'id' => get_string($user->cover_preset_id),
227 ]);
228 }
229
230 public function includeDailyChallengeUserStats(User $user)
231 {
232 return $this->item(
233 $user->dailyChallengeUserStats ?? $user->dailyChallengeUserStats()->make(),
234 new DailyChallengeUserStatsTransformer(),
235 );
236 }
237
238 public function includeFavouriteBeatmapsetCount(User $user)
239 {
240 return $this->primitive($user->profileBeatmapsetsFavourite()->count());
241 }
242
243 public function includeFollowUserMapping(User $user)
244 {
245 return $this->primitive(
246 $user->follows()->where([
247 'notifiable_type' => MorphMap::getType($user),
248 'subtype' => 'mapping',
249 ])->pluck('notifiable_id')
250 );
251 }
252
253 public function includeFollowerCount(User $user)
254 {
255 return $this->primitive($user->followerCount());
256 }
257
258 public function includeFriends(User $user)
259 {
260 return $this->collection(
261 $user->relationFriends,
262 new UserRelationTransformer()
263 );
264 }
265
266 public function includeGraveyardBeatmapsetCount(User $user)
267 {
268 return $this->primitive($user->profileBeatmapsetCountByGroupedStatus('graveyard'));
269 }
270
271 public function includeGroups(User $user)
272 {
273 return $this->collection($user->userGroupsForBadges(), new UserGroupTransformer());
274 }
275
276 public function includeGuestBeatmapsetCount(User $user)
277 {
278 return $this->primitive($user->profileBeatmapsetsGuest()->count());
279 }
280
281 public function includeIsAdmin(User $user)
282 {
283 return $this->primitive($user->isAdmin());
284 }
285
286 public function includeIsBng(User $user)
287 {
288 return $this->primitive($user->isBNG());
289 }
290
291 public function includeIsFullBn(User $user)
292 {
293 return $this->primitive($user->isFullBN());
294 }
295
296 public function includeIsGmt(User $user)
297 {
298 return $this->primitive($user->isGMT());
299 }
300
301 public function includeIsLimitedBn(User $user)
302 {
303 return $this->primitive($user->isLimitedBN());
304 }
305
306 public function includeIsModerator(User $user)
307 {
308 return $this->primitive($user->isModerator());
309 }
310
311 public function includeIsNat(User $user)
312 {
313 return $this->primitive($user->isNAT());
314 }
315
316 public function includeIsRestricted(User $user)
317 {
318 return $this->primitive($user->isRestricted());
319 }
320
321 public function includeIsSilenced(User $user)
322 {
323 return $this->primitive($user->isSilenced());
324 }
325
326 public function includeKudosu(User $user): ResourceInterface
327 {
328 return $this->primitive([
329 'available' => $user->osu_kudosavailable,
330 'total' => $user->osu_kudostotal,
331 ]);
332 }
333
334 public function includeLovedBeatmapsetCount(User $user)
335 {
336 return $this->primitive($user->profileBeatmapsetCountByGroupedStatus('loved'));
337 }
338
339 public function includeMappingFollowerCount(User $user)
340 {
341 return $this->primitive($user->mappingFollowerCount());
342 }
343
344 public function includeMonthlyPlaycounts(User $user)
345 {
346 return $this->collection(
347 $user->monthlyPlaycounts,
348 new UserMonthlyPlaycountTransformer()
349 );
350 }
351
352 public function includeNominatedBeatmapsetCount(User $user)
353 {
354 return $this->primitive($user->profileBeatmapsetsNominated()->count());
355 }
356
357 public function includePage(User $user)
358 {
359 return $this->primitive(
360 $user->userPage === null
361 ? ['html' => '', 'raw' => '']
362 : [
363 'html' => $user->userPage->bodyHTML(['modifiers' => ['profile-page']]),
364 'raw' => $user->userPage->bodyRaw,
365 ]
366 );
367 }
368
369 public function includePendingBeatmapsetCount(User $user)
370 {
371 return $this->primitive($user->profileBeatmapsetCountByGroupedStatus('pending'));
372 }
373
374 public function includePreviousUsernames(User $user)
375 {
376 return $this->primitive($user->previousUsernames()->unique()->values()->toArray());
377 }
378
379 public function includeRankHighest(User $user): ResourceInterface
380 {
381 $rankHighest = $user->rankHighests()
382 ->where('mode', Beatmap::modeInt($this->mode))
383 ->first();
384
385 return $rankHighest === null
386 ? $this->null()
387 : $this->item($rankHighest, new RankHighestTransformer());
388 }
389
390 public function includeRankHistory(User $user)
391 {
392 $rankHistoryData = $user->rankHistories()
393 ->where('mode', Beatmap::modeInt($this->mode))
394 ->first()
395 ?->setRelation('user', $user);
396
397 return $rankHistoryData === null
398 ? $this->primitive(null)
399 : $this->item($rankHistoryData, new RankHistoryTransformer());
400 }
401
402 public function includeRankedBeatmapsetCount(User $user)
403 {
404 return $this->primitive($user->profileBeatmapsetCountByGroupedStatus('ranked'));
405 }
406
407 public function includeReplaysWatchedCounts(User $user)
408 {
409 return $this->collection(
410 $user->replaysWatchedCounts,
411 new UserReplaysWatchedCountTransformer()
412 );
413 }
414
415 public function includeScoresBestCount(User $user)
416 {
417 return $this->primitive(count($user->beatmapBestScoreIds(
418 $this->mode,
419 ScoreSearchParams::showLegacyForUser(\Auth::user()),
420 )));
421 }
422
423 public function includeScoresFirstCount(User $user)
424 {
425 return $this->primitive($user->scoresFirst($this->mode, true)->count());
426 }
427
428 public function includeScoresPinnedCount(User $user)
429 {
430 return $this->primitive($user->scorePins()->forRuleset($this->mode)->withVisibleScore()->count());
431 }
432
433 public function includeScoresRecentCount(User $user)
434 {
435 return $this->primitive($user->soloScores()->recent($this->mode, false)->count());
436 }
437
438 public function includeSessionVerified(User $user)
439 {
440 return $this->primitive($user->token()?->isVerified() ?? false);
441 }
442
443 public function includeStatistics(User $user)
444 {
445 $stats = $user->statistics($this->mode);
446
447 return $this->item($stats, new UserStatisticsTransformer());
448 }
449
450 public function includeStatisticsRulesets(User $user)
451 {
452 return $this->item($user, new UserStatisticsRulesetsTransformer());
453 }
454
455 public function includeSupportLevel(User $user)
456 {
457 return $this->primitive($user->supportLevel());
458 }
459
460 public function includeTeam(User $user)
461 {
462 return ($team = $user->team) === null
463 ? $this->null()
464 : $this->item($team, new TeamTransformer());
465 }
466
467 public function includeUnreadPmCount(User $user)
468 {
469 // legacy pm has been turned off
470 return $this->primitive(0);
471 }
472
473 public function includeUserAchievements(User $user)
474 {
475 return $this->collection(
476 $user->userAchievements()->orderBy('date', 'desc')->get(),
477 new UserAchievementTransformer()
478 );
479 }
480
481 public function includeUserPreferences(User $user)
482 {
483 static $fields = [
484 'audio_autoplay',
485 'audio_muted',
486 'audio_volume',
487 'beatmapset_card_size',
488 'beatmapset_download',
489 'beatmapset_show_nsfw',
490 'beatmapset_title_show_original',
491 'comments_show_deleted',
492 'forum_posts_show_deleted',
493 'legacy_score_only',
494 'profile_cover_expanded',
495 'scoring_mode',
496 'user_list_filter',
497 'user_list_sort',
498 'user_list_view',
499 ];
500
501 $customization = $user->userProfileCustomization;
502
503 return $this->primitive($customization === null
504 ? Arr::only(UserProfileCustomization::DEFAULTS, $fields)
505 : $customization->only($fields));
506 }
507
508 public function setMode(string $mode)
509 {
510 $this->mode = $mode;
511
512 return $this;
513 }
514}