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
6declare(strict_types=1);
7
8namespace App\Models;
9
10use App\Models\Traits\WithDbCursorHelper;
11use Illuminate\Database\Eloquent\Builder;
12use Illuminate\Database\Eloquent\Relations\BelongsTo;
13use InvalidArgumentException;
14
15/**
16 * @property-read User|null $actor
17 * @property int|null $actor_id
18 * @property \Carbon\Carbon $created_at
19 * @property-read string $created_at_json
20 * @property array $details
21 * @property-read Group $group
22 * @property int $group_id
23 * @property int $id
24 * @property string $type
25 * @property-read User|null $user
26 * @property int|null $user_id
27 * @method static Builder visibleForUser(User|null $user)
28 */
29class UserGroupEvent extends Model
30{
31 use WithDbCursorHelper;
32
33 public const GROUP_ADD = 'group_add';
34 public const GROUP_REMOVE = 'group_remove';
35 public const GROUP_RENAME = 'group_rename';
36 public const USER_ADD = 'user_add';
37 public const USER_ADD_PLAYMODES = 'user_add_playmodes';
38 public const USER_REMOVE = 'user_remove';
39 public const USER_REMOVE_PLAYMODES = 'user_remove_playmodes';
40 public const USER_SET_DEFAULT = 'user_set_default';
41
42 public const UPDATED_AT = null;
43
44 protected const DEFAULT_SORT = 'id_desc';
45 protected const SORTS = [
46 'id_asc' => [
47 ['column' => 'id', 'order' => 'ASC'],
48 ],
49 'id_desc' => [
50 ['column' => 'id', 'order' => 'DESC'],
51 ],
52 ];
53
54 protected $casts = [
55 'details' => 'array',
56 'hidden' => 'boolean',
57 ];
58
59 public static function logGroupAdd(?User $actor, Group $group): void
60 {
61 static::log($actor, static::GROUP_ADD, null, $group);
62 }
63
64 public static function logGroupRemove(?User $actor, Group $group): void
65 {
66 static::log($actor, static::GROUP_REMOVE, null, $group);
67 }
68
69 public static function logGroupRename(?User $actor, Group $group, string $previousName, string $name): void
70 {
71 static::log($actor, static::GROUP_RENAME, null, $group, [
72 'details' => [
73 'group_name' => $name,
74 'previous_group_name' => $previousName,
75 ],
76 ]);
77 }
78
79 public static function logUserAdd(?User $actor, User $user, Group $group, ?array $playmodes = null): void
80 {
81 // Never log additions to the default group
82 if ($group->identifier === 'default') {
83 return;
84 }
85
86 if (empty($playmodes)) {
87 $playmodes = null;
88 }
89
90 static::log($actor, static::USER_ADD, $user, $group, [
91 'details' => compact('playmodes'),
92 ]);
93 }
94
95 public static function logUserAddPlaymodes(?User $actor, User $user, Group $group, array $playmodes): void
96 {
97 if (empty($playmodes)) {
98 throw new InvalidArgumentException('playmodes must not be empty');
99 }
100
101 static::log($actor, static::USER_ADD_PLAYMODES, $user, $group, [
102 'details' => compact('playmodes'),
103 ]);
104 }
105
106 public static function logUserRemove(?User $actor, User $user, Group $group): void
107 {
108 static::log($actor, static::USER_REMOVE, $user, $group);
109 }
110
111 public static function logUserRemovePlaymodes(?User $actor, User $user, Group $group, array $playmodes): void
112 {
113 if (empty($playmodes)) {
114 throw new InvalidArgumentException('playmodes must not be empty');
115 }
116
117 static::log($actor, static::USER_REMOVE_PLAYMODES, $user, $group, [
118 'details' => compact('playmodes'),
119 ]);
120 }
121
122 public static function logUserSetDefault(?User $actor, User $user, Group $group): void
123 {
124 static::log($actor, static::USER_SET_DEFAULT, $user, $group);
125 }
126
127 private static function log(?User $actor, string $type, ?User $user, Group $group, array $attributes = []): void
128 {
129 $attributes['details'] = array_merge(
130 [
131 'actor_name' => $actor?->username,
132 'group_name' => $group->group_name,
133 'user_name' => $user?->username,
134 ],
135 $attributes['details'] ?? [],
136 );
137
138 (new static(array_merge(
139 [
140 'actor_id' => $actor?->getKey(),
141 'group_id' => $group->getKey(),
142 'hidden' => !$group->hasListing(),
143 'type' => $type,
144 'user_id' => $user?->getKey(),
145 ],
146 $attributes,
147 )))->saveOrExplode();
148 }
149
150 public function actor(): BelongsTo
151 {
152 return $this->belongsTo(User::class, 'actor_id');
153 }
154
155 public function group(): BelongsTo
156 {
157 return $this->belongsTo(Group::class, 'group_id');
158 }
159
160 public function user(): BelongsTo
161 {
162 return $this->belongsTo(User::class, 'user_id');
163 }
164
165 public function scopeVisibleForUser(Builder $query, ?User $user): void
166 {
167 if (priv_check_user($user, 'UserGroupEventShowAll')->can()) {
168 return;
169 }
170
171 $query->where('hidden', false);
172
173 $userGroupIds = priv_check_user($user, 'IsSpecialScope')->can()
174 ? $user->groupIds()['active']
175 : [];
176
177 if (!empty($userGroupIds)) {
178 $query->orWhereIn('group_id', $userGroupIds);
179 }
180 }
181
182 public function getAttribute($key)
183 {
184 return match ($key) {
185 'actor_id',
186 'group_id',
187 'id',
188 'type',
189 'user_id' => $this->getRawAttribute($key),
190
191 'details' => json_decode($this->getRawAttribute($key), true),
192
193 'created_at' => $this->getTimeFast($key),
194
195 'created_at_json' => $this->getJsonTimeFast($key),
196
197 'actor',
198 'user' => $this->getRelationValue($key),
199
200 'group' => app('groups')->byIdOrFail($this->getRawAttribute('group_id')),
201 };
202 }
203
204 // Laravel has own hidden property
205 // TODO: https://github.com/ppy/osu-web/pull/9486#discussion_r1017831112
206 public function isHidden(): bool
207 {
208 return (bool) $this->getRawAttribute('hidden');
209 }
210}