-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support multiple mappers on beatmap difficulties #11377
base: master
Are you sure you want to change the base?
Changes from 79 commits
55dbc21
c3ee82a
72b80d7
5c697f4
c93753a
1195c44
43d9dbc
79adc14
825a01a
498e3c7
2da23ff
5655d38
716957d
f9706cf
e371792
0042a94
93a51de
fdaede5
696a102
50b8c6d
fdbd894
926ee77
eb4e738
ad768a5
6b9ab68
5bb4a44
86dfd22
581259a
6ec895d
187306d
0c84a6a
c539574
65ac60e
3732f20
2e25964
9217224
7f283ea
edce318
bc706ef
736a853
5e481aa
9b807b8
b264688
4b91a83
b3cb55a
ecbd11a
e8d4d00
1c64529
eacb7bc
d7d1b0f
c629115
405a091
7d4f3e2
4f757d4
78bff9f
dcf1d1b
58a28b4
0d3d6f0
bb48bf2
9d7845b
5d0faef
4baa745
9aa564e
7e1e92f
3c2129e
fa1e602
a70c617
a744a1f
eff4003
2d19464
77807ac
40ca43c
31059d6
0a8891d
da5cd9c
384d0b2
de8de24
198e59a
1f31a9d
620c1f7
100f320
de0841e
a24d80b
2a475b3
d587edb
2afa074
c7d6d02
9a63743
ef1721a
0de28c1
0e306c3
0601e19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
namespace App\Console\Commands; | ||
|
||
use App\Models\Beatmap; | ||
use Illuminate\Console\Command; | ||
|
||
class BeatmapsMigrateOwners extends Command | ||
{ | ||
protected $signature = 'beatmaps:migrate-owners'; | ||
|
||
protected $description = 'Migrates beatmap owners to new table.'; | ||
|
||
public function handle() | ||
{ | ||
$progress = $this->output->createProgressBar(); | ||
|
||
Beatmap::chunkById(1000, function ($beatmaps) use ($progress) { | ||
foreach ($beatmaps as $beatmap) { | ||
$beatmap->beatmapOwners()->firstOrCreate(['user_id' => $beatmap->user_id]); | ||
$progress->advance(); | ||
} | ||
}); | ||
|
||
$progress->finish(); | ||
$this->line(''); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php | ||
|
||
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
declare(strict_types=1); | ||
|
||
namespace App\Libraries\Beatmapset; | ||
|
||
use App\Exceptions\InvariantException; | ||
use App\Jobs\Notifications\BeatmapOwnerChange; | ||
use App\Models\Beatmap; | ||
use App\Models\BeatmapOwner; | ||
use App\Models\BeatmapsetEvent; | ||
use App\Models\DeletedUser; | ||
use App\Models\User; | ||
use Ds\Set; | ||
|
||
class ChangeBeatmapOwners | ||
{ | ||
private Set $userIds; | ||
|
||
public function __construct(private Beatmap $beatmap, array $newUserIds, private User $source) | ||
{ | ||
priv_check_user($source, 'BeatmapUpdateOwner', $beatmap->beatmapset)->ensureCan(); | ||
|
||
$this->userIds = new Set($newUserIds); | ||
|
||
if ($this->userIds->isEmpty()) { | ||
throw new InvariantException('user_ids must be specified'); | ||
} | ||
} | ||
|
||
public function handle(): void | ||
{ | ||
$currentOwners = new Set($this->beatmap->mappers->pluck('user_id')); | ||
if ($currentOwners->xor($this->userIds)->isEmpty()) { | ||
return; | ||
} | ||
|
||
$newUserIds = $this->userIds->diff($currentOwners); | ||
|
||
if (User::whereIn('user_id', $newUserIds->toArray())->count() !== $newUserIds->count()) { | ||
throw new InvariantException('invalid user_id'); | ||
} | ||
|
||
$this->beatmap->getConnection()->transaction(function () { | ||
$userIds = $this->userIds->toArray(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not iterate over the set itself? (or even straight out map) |
||
$params = []; | ||
|
||
foreach ($userIds as $userId) { | ||
$params[] = ['beatmap_id' => $this->beatmap->getKey(), 'user_id' => $userId]; | ||
} | ||
|
||
$this->beatmap->fill(['user_id' => $userIds[0]])->saveOrExplode(); | ||
$this->beatmap->beatmapOwners()->delete(); | ||
BeatmapOwner::insert($params); | ||
|
||
$this->beatmap->refresh(); | ||
|
||
// TODO: use select instead (needs newer laravel) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how would the deleted user username fallback work with select though? |
||
$newUsers = $this->beatmap->mappers->map( | ||
fn ($user) => ['id' => $user->user_id, 'username' => ($user ?? new DeletedUser())->username], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't the |
||
)->all(); | ||
$beatmapset = $this->beatmap->beatmapset; | ||
|
||
BeatmapsetEvent::log(BeatmapsetEvent::BEATMAP_OWNER_CHANGE, $this->source, $beatmapset, [ | ||
'beatmap_id' => $this->beatmap->getKey(), | ||
'beatmap_version' => $this->beatmap->version, | ||
'new_user_id' => $this->beatmap->user_id, | ||
'new_user_username' => ($this->beatmap->user ?? new DeletedUser())->username, | ||
'new_users' => $newUsers, | ||
])->saveOrExplode(); | ||
|
||
$beatmapset->update(['eligible_main_rulesets' => null]); | ||
}); | ||
|
||
(new BeatmapOwnerChange($this->beatmap, $this->source))->dispatch(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,14 +7,17 @@ | |
|
||
use App\Exceptions\InvariantException; | ||
use App\Jobs\EsDocument; | ||
use App\Libraries\Beatmapset\ChangeBeatmapOwners; | ||
use App\Libraries\Transactions\AfterCommit; | ||
use DB; | ||
use Ds\Set; | ||
use Illuminate\Database\Eloquent\Builder; | ||
use Illuminate\Database\Eloquent\Collection; | ||
use Illuminate\Database\Eloquent\SoftDeletes; | ||
|
||
/** | ||
* @property int $approved | ||
* @property \Illuminate\Database\Eloquent\Collection $beatmapDiscussions BeatmapDiscussion | ||
* @property-read Collection<BeatmapDiscussion> $beatmapDiscussions | ||
* @property int $beatmap_id | ||
* @property Beatmapset $beatmapset | ||
* @property int|null $beatmapset_id | ||
|
@@ -29,20 +32,22 @@ | |
* @property float $diff_drain | ||
* @property float $diff_overall | ||
* @property float $diff_size | ||
* @property \Illuminate\Database\Eloquent\Collection $difficulty BeatmapDifficulty | ||
* @property \Illuminate\Database\Eloquent\Collection $difficultyAttribs BeatmapDifficultyAttrib | ||
* @property-read Collection<BeatmapDifficulty> $difficulty | ||
* @property-read Collection<BeatmapDifficultyAttrib> $difficultyAttribs | ||
* @property float $difficultyrating | ||
* @property \Illuminate\Database\Eloquent\Collection $failtimes BeatmapFailtimes | ||
* @property-read Collection<BeatmapFailtimes> $failtimes | ||
* @property string|null $filename | ||
* @property int $hit_length | ||
* @property \Carbon\Carbon $last_update | ||
* @property int $max_combo | ||
* @property mixed $mode | ||
* @property-read Collection<User> $mappers | ||
* @property int $passcount | ||
* @property int $playcount | ||
* @property int $playmode | ||
* @property int $score_version | ||
* @property int $total_length | ||
* @property User $user | ||
* @property int $user_id | ||
* @property string $version | ||
* @property string|null $youtube_preview | ||
|
@@ -107,6 +112,11 @@ public function baseMaxCombo() | |
return $this->difficultyAttribs()->noMods()->maxCombo(); | ||
} | ||
|
||
public function beatmapOwners() | ||
{ | ||
return $this->hasMany(BeatmapOwner::class); | ||
} | ||
|
||
public function beatmapset() | ||
{ | ||
return $this->belongsTo(Beatmapset::class, 'beatmapset_id')->withTrashed(); | ||
|
@@ -256,6 +266,7 @@ public function getAttribute($key) | |
|
||
'diff_size' => $this->getDiffSize(), | ||
'difficultyrating' => $this->getDifficultyrating(), | ||
'mappers' => $this->getMappers(), | ||
'mode' => $this->getMode(), | ||
'version' => $this->getVersion(), | ||
|
||
|
@@ -300,22 +311,9 @@ public function maxCombo() | |
return $maxCombo?->value; | ||
} | ||
|
||
public function setOwner($newUserId) | ||
public function setOwner(array $newUserIds, User $source): void | ||
{ | ||
if ($newUserId === null) { | ||
throw new InvariantException('user_id must be specified'); | ||
} | ||
|
||
if (User::find($newUserId) === null) { | ||
throw new InvariantException('invalid user_id'); | ||
} | ||
|
||
if ($newUserId === $this->user_id) { | ||
throw new InvariantException('the specified user_id is already the owner'); | ||
} | ||
|
||
$this->fill(['user_id' => $newUserId])->saveOrExplode(); | ||
$this->beatmapset->update(['eligible_main_rulesets' => null]); | ||
(new ChangeBeatmapOwners($this, $newUserIds, $source))->handle(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it all that useful over just using the class directly |
||
} | ||
|
||
public function status() | ||
|
@@ -376,6 +374,30 @@ private function getDiffSize() | |
return $value; | ||
} | ||
|
||
private function getMappers(): Collection | ||
{ | ||
$beatmapOwners = $this->beatmapOwners()->pluck('user_id'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this will ignore preloaded relation |
||
|
||
$mappers = User::whereIn('user_id', $beatmapOwners)->get(); | ||
// compatiblity for anything that isn't writing to beatmap_owners yet. | ||
if ($mappers->find($this->user_id) === null && $this->user !== null) { | ||
$mappers->prepend($this->user); | ||
} | ||
|
||
// Add deleted/missing users. | ||
if ($beatmapOwners->count() !== $mappers->count()) { | ||
$beatmapOwnersSet = new Set($beatmapOwners); | ||
$mappersSet = new Set($mappers->pluck('user_id')->toArray()); | ||
|
||
$missingIds = $beatmapOwnersSet->diff($mappersSet); | ||
foreach ($missingIds as $id) { | ||
$mappers->push(new DeletedUser(['user_id' => $id])); | ||
} | ||
} | ||
|
||
return $mappers; | ||
} | ||
|
||
private function getMode() | ||
{ | ||
return static::modeStr($this->playmode); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?php | ||
|
||
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0. | ||
// See the LICENCE file in the repository root for full licence text. | ||
|
||
namespace App\Models; | ||
|
||
/** | ||
* @property Beatmap $beatmap | ||
* @property int $beatmap_id | ||
* @property User $user | ||
* @property int $user_id | ||
*/ | ||
class BeatmapOwner extends Model | ||
{ | ||
public $incrementing = false; | ||
public $timestamps = false; | ||
|
||
protected $primaryKey = ':composite'; | ||
protected $primaryKeys = ['beatmap_id', 'user_id']; | ||
|
||
public function beatmap() | ||
{ | ||
return $this->belongsTo(Beatmap::class, 'beatmap_id'); | ||
} | ||
|
||
public function user() | ||
{ | ||
return $this->belongsTo(User::class, 'user_id'); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some kind of limit would be useful