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\Models;
7
8use Carbon\Carbon;
9
10/**
11 * @property Beatmapset $beatmapset
12 * @property int $beatmapset_id
13 * @property string|null $comment
14 * @property \Carbon\Carbon|null $created_at
15 * @property int $id
16 * @property mixed|null $type
17 * @property \Carbon\Carbon|null $updated_at
18 * @property User $user
19 * @property int|null $user_id
20 */
21class BeatmapsetEvent extends Model
22{
23 const NOMINATE = 'nominate';
24 const LOVE = 'love';
25 const REMOVE_FROM_LOVED = 'remove_from_loved';
26 const QUALIFY = 'qualify';
27 const DISQUALIFY = 'disqualify';
28 const APPROVE = 'approve';
29 const RANK = 'rank';
30
31 const KUDOSU_ALLOW = 'kudosu_allow';
32 const KUDOSU_DENY = 'kudosu_deny';
33 const KUDOSU_GAIN = 'kudosu_gain';
34 const KUDOSU_LOST = 'kudosu_lost';
35 const KUDOSU_RECALCULATE = 'kudosu_recalculate';
36
37 const ISSUE_RESOLVE = 'issue_resolve';
38 const ISSUE_REOPEN = 'issue_reopen';
39
40 const DISCUSSION_LOCK = 'discussion_lock';
41 const DISCUSSION_UNLOCK = 'discussion_unlock';
42
43 const DISCUSSION_DELETE = 'discussion_delete';
44 const DISCUSSION_RESTORE = 'discussion_restore';
45
46 const DISCUSSION_POST_DELETE = 'discussion_post_delete';
47 const DISCUSSION_POST_RESTORE = 'discussion_post_restore';
48
49 const NOMINATION_RESET = 'nomination_reset';
50 const NOMINATION_RESET_RECEIVED = 'nomination_reset_received';
51
52 const GENRE_EDIT = 'genre_edit';
53 const LANGUAGE_EDIT = 'language_edit';
54 const NSFW_TOGGLE = 'nsfw_toggle';
55 const OFFSET_EDIT = 'offset_edit';
56 const TAGS_EDIT = 'tags_edit';
57
58 const BEATMAP_OWNER_CHANGE = 'beatmap_owner_change';
59
60 public static function log($type, $user, $object, $extraData = [])
61 {
62 if ($object instanceof BeatmapDiscussionPost) {
63 $discussionPostId = $object->getKey();
64 $discussionId = $object->beatmap_discussion_id;
65 $beatmapsetId = $object->beatmapDiscussion->beatmapset_id;
66 } elseif ($object instanceof BeatmapDiscussion) {
67 $discussionId = $object->getKey();
68 $beatmapsetId = $object->beatmapset_id;
69 } elseif ($object instanceof Beatmapset) {
70 $beatmapsetId = $object->getKey();
71 }
72
73 return new static([
74 'beatmapset_id' => $beatmapsetId,
75 'user_id' => isset($user) ? $user->getKey() : null,
76 'type' => $type,
77 'comment' => array_merge([
78 'beatmap_discussion_id' => $discussionId ?? null,
79 'beatmap_discussion_post_id' => $discussionPostId ?? null,
80 ], $extraData),
81 ]);
82 }
83
84 public static function search($rawParams = [])
85 {
86 [$query, $params] = static::searchQueryAndParams($rawParams);
87
88 $searchByUser = present($rawParams['user'] ?? null);
89 $isModerator = $rawParams['is_moderator'] ?? false;
90
91 if ($searchByUser) {
92 $params['user'] = $rawParams['user'];
93 $findAll = $isModerator || (($rawParams['current_user_id'] ?? null) === $rawParams['user']);
94 $user = User::lookup($params['user'], null, $findAll);
95
96 if ($user === null) {
97 $query->none();
98 } else {
99 $query->where('user_id', '=', $user->getKey());
100 }
101 }
102
103 if (present($rawParams['beatmapset_id'] ?? null)) {
104 $params['beatmapset_id'] = $rawParams['beatmapset_id'];
105 $query->where('beatmapset_id', '=', $params['beatmapset_id']);
106 }
107
108 $sortParam = presence(get_string($rawParams['sort'] ?? null));
109 if (isset($sortParam)) {
110 $sort = explode('_', strtolower($sortParam));
111
112 if (in_array($sort[0] ?? null, ['id'], true)) {
113 $sortField = $sort[0];
114 }
115
116 if (in_array($sort[1] ?? null, ['asc', 'desc'], true)) {
117 $sortOrder = $sort[1];
118 }
119 }
120
121 $sortField ??= 'id';
122 $sortOrder ??= 'desc';
123
124 if ($sortField !== 'id' && $sortOrder !== 'desc') {
125 $params['sort'] = "{$sortField}_{$sortOrder}";
126 }
127
128 $query->orderBy($sortField, $sortOrder);
129
130 $params['types'] = [];
131
132 if (get_string($rawParams['type'] ?? null) !== null) {
133 $params['types'][] = $rawParams['type'];
134 }
135
136 if (isset($rawParams['types'])) {
137 $params['types'] = array_merge($params['types'], get_arr($rawParams['types'], 'get_string') ?? []);
138 }
139
140 if ($searchByUser) {
141 $allowedTypes = static::types('public');
142 if ($isModerator) {
143 $allowedTypes = array_merge($allowedTypes, static::types('moderation'));
144 }
145 if ($rawParams['is_kudosu_moderator'] ?? false) {
146 $allowedTypes = array_merge($allowedTypes, static::types('kudosuModeration'));
147 }
148 } else {
149 $allowedTypes = static::types('all');
150 }
151
152 $params['types'] = array_intersect($params['types'], $allowedTypes);
153
154 if (empty($params['types'])) {
155 if ($searchByUser) {
156 $query->whereIn('type', $allowedTypes);
157 }
158 } else {
159 $query->whereIn('type', $params['types']);
160 }
161
162 if (isset($rawParams['min_date'])) {
163 $timestamp = strtotime($rawParams['min_date']);
164
165 if ($timestamp !== false) {
166 $minDate = Carbon::createFromTimestamp($timestamp)->startOfDay();
167 $params['min_date'] = json_date($minDate);
168 $query->where('created_at', '>=', $minDate);
169 }
170 }
171
172 if (isset($rawParams['max_date'])) {
173 $timestamp = strtotime($rawParams['max_date']);
174
175 if ($timestamp !== false) {
176 $maxDate = Carbon::createFromTimestamp($timestamp)->endOfDay();
177 $params['max_date'] = json_date($maxDate);
178 $query->where('created_at', '<=', $maxDate);
179 }
180 }
181
182 return ['query' => $query, 'params' => $params];
183 }
184
185 /**
186 * Currently used for:
187 * - generating type filter checkboxes in events index page
188 * - searching by user should limit the allowed types
189 * - checking whether or not user id should be visible
190 * Order affects how they're displayed.
191 */
192 public static function types($privilege)
193 {
194 static $ret;
195
196 if ($ret === null) {
197 $ret = [
198 'public' => [
199 static::NOMINATE,
200 static::QUALIFY,
201 static::RANK,
202 static::LOVE,
203 static::NOMINATION_RESET,
204 static::NOMINATION_RESET_RECEIVED,
205 static::DISQUALIFY,
206 static::REMOVE_FROM_LOVED,
207
208 static::KUDOSU_GAIN,
209 static::KUDOSU_LOST,
210
211 static::GENRE_EDIT,
212 static::LANGUAGE_EDIT,
213 static::NSFW_TOGGLE,
214 static::OFFSET_EDIT,
215
216 static::ISSUE_RESOLVE,
217 static::ISSUE_REOPEN,
218
219 static::BEATMAP_OWNER_CHANGE,
220 ],
221 'kudosuModeration' => [
222 static::KUDOSU_ALLOW,
223 static::KUDOSU_DENY,
224 ],
225 'moderation' => [
226 static::APPROVE, // not actually used
227
228 static::KUDOSU_RECALCULATE,
229
230 static::DISCUSSION_DELETE,
231 static::DISCUSSION_RESTORE,
232
233 static::DISCUSSION_POST_DELETE,
234 static::DISCUSSION_POST_RESTORE,
235 ],
236 ];
237 }
238
239 if ($privilege === 'all' && !isset($ret['all'])) {
240 $all = [];
241
242 foreach ($ret as $_priv => $types) {
243 $all = array_merge($all, $types);
244 }
245
246 $ret['all'] = $all;
247 }
248
249 return $ret[$privilege];
250 }
251
252 public function beatmapset()
253 {
254 // FIXME: consistency with BeatmapDiscussion which includes deleted.
255 return $this->belongsTo(Beatmapset::class, 'beatmapset_id');
256 }
257
258 public function getBeatmapDiscussionIdAttribute()
259 {
260 return $this->comment['beatmap_discussion_id'] ?? null;
261 }
262
263 public function getNominationModesAttribute()
264 {
265 if ($this->type !== self::NOMINATE) {
266 return null;
267 }
268
269 return $this->comment['modes'] ?? [];
270 }
271
272 public function beatmapDiscussion()
273 {
274 return $this->belongsTo(BeatmapDiscussion::class, 'beatmap_discussion_id');
275 }
276
277 public function user()
278 {
279 return $this->belongsTo(User::class, 'user_id');
280 }
281
282 public function scopeNominations($query)
283 {
284 return $query->where('type', self::NOMINATE);
285 }
286
287 public function scopeNominationResetReceiveds($query)
288 {
289 return $query->where('type', self::NOMINATION_RESET_RECEIVED);
290 }
291
292 public function scopeNominationResets($query)
293 {
294 return $query->where('type', self::NOMINATION_RESET);
295 }
296
297 public function scopeDisqualifications($query)
298 {
299 return $query->where('type', self::DISQUALIFY);
300 }
301
302 public function scopeDisqualificationAndNominationResetEvents($query)
303 {
304 return $query->whereIn('type', [self::DISQUALIFY, self::NOMINATION_RESET]);
305 }
306
307 public function getCommentAttribute($value)
308 {
309 return json_decode($value ?? '', true) ?? $value;
310 }
311
312 public function setCommentAttribute($value)
313 {
314 $this->attributes['comment'] = is_array($value) ? json_encode($value) : $value;
315 }
316}