the browser-facing portion of osu!
at master 346 lines 11 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\Libraries; 7 8use App\Libraries\BeatmapsetDiscussion\Review; 9use App\Models\Beatmap; 10use App\Models\BeatmapDiscussion; 11use App\Models\BeatmapDiscussionPost; 12use App\Models\BeatmapDiscussionVote; 13use App\Models\Beatmapset; 14use App\Models\BeatmapsetEvent; 15use App\Models\User; 16use App\Traits\Memoizes; 17use App\Transformers\UserTransformer; 18use Ds\Set; 19use Illuminate\Database\Eloquent\Relations\MorphTo; 20use Illuminate\Pagination\LengthAwarePaginator; 21 22class ModdingHistoryEventsBundle 23{ 24 use Memoizes; 25 26 const KUDOSU_PER_PAGE = 5; 27 28 protected $isModerator; 29 protected $isKudosuModerator; 30 protected $searchParams; 31 32 private $params; 33 private $total; 34 private $user; 35 private $withExtras = false; // TODO: change to includes list instead. 36 37 public static function forProfile(User $user, array $searchParams) 38 { 39 $searchParams['limit'] = 10; 40 $searchParams['sort'] = 'id_desc'; 41 42 $obj = static::forListing($user, $searchParams); 43 $obj->withExtras = true; 44 45 return $obj; 46 } 47 48 public static function forListing(?User $user, array $searchParams) 49 { 50 $obj = new static(); 51 $obj->user = $user; 52 $obj->searchParams = $searchParams; 53 $obj->isModerator = priv_check('BeatmapDiscussionModerate')->can(); 54 $obj->isKudosuModerator = priv_check('BeatmapDiscussionAllowOrDenyKudosu')->can(); 55 56 $obj->searchParams['is_moderator'] = $obj->isModerator; 57 58 if (!$obj->isModerator) { 59 $obj->searchParams['with_deleted'] = false; 60 } 61 62 return $obj; 63 } 64 65 public function getPaginator() 66 { 67 $events = $this->getEvents(); 68 $params = $this->params; 69 70 return new LengthAwarePaginator( 71 $events, 72 $this->total, // set in getEvents() 73 $params['limit'], 74 $params['page'], 75 [ 76 'path' => LengthAwarePaginator::resolveCurrentPath(), 77 'query' => $params, 78 ] 79 ); 80 } 81 82 public function getParams() 83 { 84 return $this->params; 85 } 86 87 public function toArray(): array 88 { 89 return $this->memoize(__FUNCTION__, function () { 90 $array = [ 91 'events' => json_collection( 92 $this->getEvents(), 93 'BeatmapsetEvent', 94 ['discussion.starting_post', 'beatmapset.user'] 95 ), 96 'reviewsConfig' => Review::config(), 97 'users' => json_collection( 98 $this->getUsers(), 99 'UserCompact', 100 ['groups'] 101 ), 102 ]; 103 104 if ($this->withExtras) { 105 $array['beatmaps'] = json_collection( 106 $this->getBeatmaps(), 107 'Beatmap' 108 ); 109 110 $array['beatmapsets'] = json_collection( 111 $this->getBeatmapsets(), 112 'Beatmapset' 113 ); 114 115 $array['discussions'] = json_collection( 116 $this->getDiscussions(), 117 'BeatmapDiscussion', 118 ['starting_post', 'current_user_attributes'] 119 ); 120 121 $array['posts'] = json_collection( 122 $this->getPosts(), 123 'BeatmapDiscussionPost', 124 // TODO: should get beatmapset from top level beatmapset key instead of embedded property. 125 ['beatmap_discussion.beatmapset.availability'] 126 ); 127 128 $array['votes'] = $this->getVotes(); 129 130 if ($this->user !== null) { 131 $kudosu = $this->user 132 ->receivedKudosu() 133 ->with('post', 'post.topic', 'giver') 134 ->with(['kudosuable' => function (MorphTo $morphTo) { 135 $morphTo->morphWith([BeatmapDiscussion::class => ['beatmap', 'beatmapset']]); 136 }]) 137 ->orderBy('exchange_id', 'desc') 138 ->limit(static::KUDOSU_PER_PAGE + 1) 139 ->get(); 140 141 $array['extras'] = [ 142 'recentlyReceivedKudosu' => json_collection($kudosu, 'KudosuHistory'), 143 ]; 144 // only recentlyReceivedKudosu is set, do we even need it? 145 // every other item has a show more link that goes to a listing. 146 $array['perPage'] = [ 147 'recentlyReceivedKudosu' => static::KUDOSU_PER_PAGE, 148 ]; 149 150 $array['user'] = json_item( 151 $this->user, 152 (new UserTransformer())->setMode($this->user->playmode), 153 [ 154 ...UserTransformer::PROFILE_HEADER_INCLUDES, 155 'graveyard_beatmapset_count', 156 'loved_beatmapset_count', 157 'pending_beatmapset_count', 158 'ranked_beatmapset_count', 159 'statistics', 160 'statistics.country_rank', 161 'statistics.rank', 162 ] 163 ); 164 } 165 } 166 167 return $array; 168 }); 169 } 170 171 private function getBeatmaps() 172 { 173 return $this->memoize(__FUNCTION__, function () { 174 if (!$this->withExtras) { 175 return collect(); 176 } 177 178 $beatmapsetId = $this->getBeatmapsets() 179 ->pluck('beatmapset_id'); 180 181 return Beatmap::whereIn('beatmapset_id', $beatmapsetId)->get(); 182 }); 183 } 184 185 private function getBeatmapsets() 186 { 187 return $this->memoize(__FUNCTION__, function () { 188 if (!$this->withExtras) { 189 return collect(); 190 } 191 192 $beatmapsetId = $this->getDiscussions() 193 ->pluck('beatmapset_id') 194 ->unique() 195 ->toArray(); 196 197 return Beatmapset::whereIn('beatmapset_id', $beatmapsetId)->get(); 198 }); 199 } 200 201 private function getDiscussions() 202 { 203 return $this->memoize(__FUNCTION__, function () { 204 static $includes = [ 205 'beatmap', 206 'beatmapDiscussionVotes', 207 'beatmapset', 208 'startingPost', 209 ]; 210 211 if (!$this->withExtras) { 212 return collect(); 213 } 214 215 $parents = BeatmapDiscussion::search($this->searchParams); 216 $parents['query']->with($includes); 217 218 if ($this->isModerator) { 219 $parents['query']->visibleWithTrashed(); 220 } else { 221 $parents['query']->visible(); 222 } 223 224 $discussions = $parents['query']->get(); 225 226 $children = BeatmapDiscussion::whereIn('parent_id', $discussions->pluck('id'))->with($includes); 227 228 if ($this->isModerator) { 229 $children->visibleWithTrashed(); 230 } else { 231 $children->visible(); 232 } 233 234 return $discussions->merge($children->get()); 235 }); 236 } 237 238 private function getEvents() 239 { 240 return $this->memoize(__FUNCTION__, function () { 241 $events = BeatmapsetEvent::search($this->searchParams); 242 // beatmapset has global scopes with deleted_at and active but these are not indexed, 243 // which makes whereHas('beatmapset') unusable. 244 $events['query'] = $events['query']->with([ 245 'beatmapset.user', 246 'beatmapDiscussion.beatmapset', 247 'beatmapDiscussion.startingPost', 248 ]); 249 250 if ($this->isModerator) { 251 $events['query']->with(['beatmapset' => function ($query) { 252 $query->withTrashed(); 253 }]); 254 } 255 256 // just for the paginator 257 $this->total = $events['query']->realCount(); 258 $this->params = $events['params']; 259 260 return $events['query']->get(); 261 }); 262 } 263 264 private function getPosts() 265 { 266 return $this->memoize(__FUNCTION__, function () { 267 if (!$this->withExtras) { 268 return collect(); 269 } 270 271 $posts = BeatmapDiscussionPost::search($this->searchParams); 272 $posts['query']->with([ 273 'beatmapDiscussion.beatmap', 274 'beatmapDiscussion.beatmapset', 275 ]); 276 277 if (!$this->isModerator) { 278 $posts['query']->visible(); 279 } 280 281 return $posts['query']->get(); 282 }); 283 } 284 285 private function getUsers() 286 { 287 return $this->memoize(__FUNCTION__, function () { 288 $discussions = $this->getDiscussions(); 289 $events = $this->getEvents(); 290 $posts = $this->getPosts(); 291 $votes = $this->getVotes(); 292 293 $userIds = new Set(); 294 foreach ($discussions as $discussion) { 295 $userIds->add( 296 $discussion->user_id, 297 $discussion->startingPost->last_editor_id 298 ); 299 } 300 301 $userIds->add( 302 ...$posts->pluck('user_id'), 303 ...$posts->pluck('last_editor_id'), 304 ...$events->pluck('user_id'), 305 ...$events->pluck('beatmapDiscussion')->pluck('user_id'), 306 ...$votes['given']->pluck('user_id'), 307 ...$votes['received']->pluck('user_id') 308 ); 309 310 if ($this->user !== null) { 311 // Always add current user to the result array (assuming no need to do too many additional preloads). 312 // This prevents them from potentially get removed by the `default` scope. 313 $userIds->remove($this->user->getKey()); 314 } 315 316 $users = User::whereIn('user_id', $userIds->toArray())->with('userGroups'); 317 if (!$this->isModerator) { 318 $users->default(); 319 } 320 321 $users = $users->get(); 322 if ($this->user !== null) { 323 $users->push($this->user); 324 } 325 326 return $users; 327 }); 328 } 329 330 private function getVotes() 331 { 332 return $this->memoize(__FUNCTION__, function () { 333 if ($this->withExtras && $this->user !== null) { 334 return [ 335 'given' => BeatmapDiscussionVote::recentlyGivenByUser($this->user->getKey()), 336 'received' => BeatmapDiscussionVote::recentlyReceivedByUser($this->user->getKey()), 337 ]; 338 } else { 339 return [ 340 'given' => collect(), 341 'received' => collect(), 342 ]; 343 } 344 }); 345 } 346}