the browser-facing portion of osu!
at master 8.2 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\Models\Comment; 9use App\Models\CommentVote; 10use App\Models\User; 11use Ds\Set; 12use Illuminate\Database\Eloquent\Collection; 13 14class CommentBundle 15{ 16 public int $depth; 17 public bool $includeDeleted; 18 public bool $includePinned; 19 public CommentBundleParams $params; 20 21 private ?Comment $comment; 22 private ?User $user; 23 24 public static function forComment(Comment $comment, bool $includeNested = false) 25 { 26 $options = ['comment' => $comment]; 27 28 if ($includeNested) { 29 $options['params'] = ['parent_id' => $comment->getKey()]; 30 } 31 32 return new static($comment->commentable, $options); 33 } 34 35 public static function forEmbed($commentable) 36 { 37 return new static($commentable, ['params' => ['parent_id' => 0]]); 38 } 39 40 41 public function __construct(private ?Commentable $commentable, array $options = []) 42 { 43 $this->user = auth()->user(); 44 45 $this->params = new CommentBundleParams($options['params'] ?? [], $this->user); 46 47 $this->comment = $options['comment'] ?? null; 48 $this->depth = $options['depth'] ?? 2; 49 $this->includeDeleted = isset($commentable); 50 $this->includePinned = isset($commentable); 51 } 52 53 public function toArray() 54 { 55 $hasMore = false; 56 $includedComments = collect(); 57 $pinnedComments = collect(); 58 59 // Either use the provided comment as a base, or look for matching comments. 60 if (isset($this->comment)) { 61 $comments = new Collection([$this->comment]); 62 if ($this->comment->parent !== null) { 63 $includedComments->push($this->comment->parent); 64 } 65 } else { 66 $comments = $this->getComments($this->commentsQuery(), false); 67 if ($comments->count() > $this->params->limit) { 68 $hasMore = true; 69 $comments->pop(); 70 } 71 } 72 73 // Get parents when listing comments index or loading comment replies 74 if ($this->commentable === null || $this->params->parentId !== null) { 75 $parentIds = array_reject_null($comments->pluck('parent_id')); 76 if (count($parentIds) > 0) { 77 $parents = $this->getComments(Comment::whereIn('id', $parentIds)); 78 $includedComments = $includedComments->concat($parents); 79 } 80 } 81 82 $commentIds = new Set($comments->pluck('id')); 83 84 // Get nested comments 85 if ($this->params->parentId !== null) { 86 $nestedParentIds = $commentIds->toArray(); 87 88 for ($i = 0; $i < $this->depth; $i++) { 89 $nestedComments = $this->getComments(Comment::whereIn('parent_id', $nestedParentIds)); 90 $includedComments = $includedComments->concat($nestedComments); 91 $nestedParentIds = array_reject_null($nestedComments->pluck('id')); 92 if (count($nestedParentIds) === 0) { 93 break; 94 } 95 } 96 } 97 98 $includedComments = $includedComments 99 ->unique('id', true) 100 ->reject(fn ($comment) => $commentIds->contains($comment->getKey())); 101 102 if ($this->includePinned) { 103 $pinnedComments = $this->getComments($this->commentsQuery()->where('pinned', true), true, true); 104 } 105 106 $allComments = $comments->concat($includedComments)->concat($pinnedComments); 107 $allComments->load('commentable'); 108 109 $result = [ 110 'comments' => json_collection($comments, 'Comment'), 111 'has_more' => $hasMore, 112 'has_more_id' => $this->params->parentId, 113 'included_comments' => json_collection($includedComments, 'Comment'), 114 'pinned_comments' => json_collection($pinnedComments, 'Comment'), 115 'user_votes' => $this->getUserVotes($allComments), 116 'user_follow' => $this->getUserFollow(), 117 'users' => json_collection($this->getUsers($allComments), 'UserCompact'), 118 'sort' => $this->params->sort, 119 'cursor' => $this->params->cursorHelper->next($comments), 120 ]; 121 122 if ($this->params->userId !== null) { 123 $result['user'] = json_item(User::find($this->params->userId), 'UserCompact'); 124 } 125 126 if ($this->params->parentId === 0 || $this->params->parentId === null) { 127 $result['top_level_count'] = $this->commentsQuery()->whereNull('parent_id')->count(); 128 $result['total'] = $this->commentsQuery()->count(); 129 } 130 131 $commentables = $comments->pluck('commentable'); 132 // Always include initial commentable in so it can be used for attributes 133 // check even when there's no comment on it. 134 if ($this->commentable !== null) { 135 $commentables[] = $this->commentable; 136 } 137 $commentables = $commentables->uniqueStrict('commentable_identifier')->concat([null]); 138 $result['commentable_meta'] = json_collection($commentables, 'CommentableMeta'); 139 140 return $result; 141 } 142 143 public function commentsQuery() 144 { 145 if (isset($this->commentable)) { 146 $query = $this->commentable->comments(); 147 } else { 148 $query = Comment::select(); 149 } 150 151 if ($this->params->userId !== null) { 152 $query->where('user_id', $this->params->userId); 153 } 154 155 return $query; 156 } 157 158 // This is named explictly for the paginator because there's another count 159 // in ::toArray() which always includes deleted comments. 160 public function countForPaginator() 161 { 162 $query = $this->commentsQuery(); 163 164 if (!$this->includeDeleted) { 165 $query->withoutTrashed(); 166 } 167 $query->select('id')->limit($GLOBALS['cfg']['osu']['pagination']['max_count'])->unorder(); 168 169 return Comment::from($query)->count(); 170 } 171 172 private function getComments($query, $isChildren = true, $pinnedOnly = false) 173 { 174 $cursorHelper = $pinnedOnly 175 ? Comment::makeDbCursorHelper('new') 176 : $this->params->cursorHelper; 177 $queryLimit = $this->params->limit; 178 179 if (!$isChildren) { 180 if ($this->params->filterByParentId()) { 181 $query->where(['parent_id' => $this->params->parentIdForWhere()]); 182 } 183 184 $queryLimit++; 185 186 if ($this->params->after === null) { 187 $cursor = $this->params->cursor; 188 } else { 189 $lastComment = Comment::findOrFail($this->params->after); 190 $cursor = $cursorHelper->next([$lastComment]); 191 } 192 193 if ($cursor === null) { 194 $query->offset(max_offset($this->params->page, $this->params->limit)); 195 } 196 } 197 198 $query->cursorSort($cursorHelper, $cursor ?? null); 199 200 if (!$this->includeDeleted) { 201 $query->whereNull('deleted_at'); 202 } 203 204 if (!$pinnedOnly) { 205 $query->limit($queryLimit); 206 } 207 208 return $query->get(); 209 } 210 211 private function getUserFollow() 212 { 213 return $this->commentable !== null && 214 $this->user !== null && 215 $this 216 ->user 217 ->follows() 218 ->whereNotifiable($this->commentable) 219 ->where(['subtype' => 'comment']) 220 ->exists(); 221 } 222 223 private function getUserVotes($comments) 224 { 225 if ($this->user === null) { 226 return []; 227 } 228 229 $ids = $comments->pluck('id'); 230 231 return CommentVote::where(['user_id' => $this->user->getKey()]) 232 ->whereIn('comment_id', $ids) 233 ->pluck('comment_id'); 234 } 235 236 private function getUsers($comments) 237 { 238 $userIds = $comments->pluck('user_id') 239 ->concat($comments->pluck('edited_by_id')); 240 241 if (priv_check('CommentModerate')->can()) { 242 $userIds = $userIds->concat($comments->pluck('deleted_by_id')); 243 } 244 245 return User::whereIn('user_id', array_reject_null($userIds))->get(); 246 } 247}