Skip to content

Commit

Permalink
Merge pull request #11667 from bdach/destroy-rooms
Browse files Browse the repository at this point in the history
Add endpoint for closing playlists within grace period of creation
  • Loading branch information
nanaya authored Nov 20, 2024
2 parents 1fbc73b + d066a34 commit 65ca10d
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 1 deletion.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ CLIENT_CHECK_VERSION=false
# USER_PROFILE_SCORES_NOTICE=

# MULTIPLAYER_MAX_ATTEMPTS_LIMIT=128
# MULTIPLAYER_ROOM_CLOSE_GRACE_PERIOD_MINUTES=5

# NOTIFICATION_QUEUE=notification
# NOTIFICATION_REDIS_HOST=127.0.0.1
Expand Down
7 changes: 7 additions & 0 deletions app/Http/Controllers/Multiplayer/RoomsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public function __construct()
$this->middleware('require-scopes:public', ['only' => ['index', 'leaderboard', 'show']]);
}

public function destroy($id)
{
Room::findOrFail($id)->endGame(\Auth::user());

return response(null, 204);
}

/**
* Get Multiplayer Rooms
*
Expand Down
20 changes: 20 additions & 0 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,26 @@ public function startGame(User $host, array $rawParams, array $extraParams = [])
return $this->fresh();
}

/**
* @throws InvariantException
*/
public function endGame(User $requestingUser)
{
priv_check_user($requestingUser, 'MultiplayerRoomDestroy', $this)->ensureCan();

if ($this->isRealtime()) {
throw new InvariantException('Realtime rooms cannot be closed.');
}

$gracePeriodMinutes = $GLOBALS['cfg']['osu']['multiplayer']['room_close_grace_period_minutes'];
if ($this->starts_at->addMinutes($gracePeriodMinutes)->isPast()) {
throw new InvariantException('The grace period for closing this room has expired.');
}

$this->ends_at = now();
$this->save();
}

public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId)
{
priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan();
Expand Down
20 changes: 20 additions & 0 deletions app/Singletons/OsuAuthorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,26 @@ public function checkMultiplayerRoomCreate(?User $user): string
return 'ok';
}

/**
* @param User|null $user
* @param Room $room
* @return string
* @throws AuthorizationCheckException
*/
public function checkMultiplayerRoomDestroy(?User $user, Room $room): string
{
$prefix = 'room.destroy.';

$this->ensureLoggedIn($user);
$this->ensureCleanRecord($user);

if ($room->user_id !== $user->getKey()) {
return $prefix.'not_owner';
}

return 'ok';
}

/**
* @param User|null $user
* @return string
Expand Down
1 change: 1 addition & 0 deletions config/osu.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
],
'multiplayer' => [
'max_attempts_limit' => get_int(env('MULTIPLAYER_MAX_ATTEMPTS_LIMIT')) ?? 128,
'room_close_grace_period_minutes' => get_int(env('MULTIPLAYER_ROOM_CLOSE_GRACE_PERIOD_MINUTES')) ?? 5,
],
'notification' => [
'endpoint' => presence(env('NOTIFICATION_ENDPOINT'), '/home/notifications/feed'),
Expand Down
6 changes: 6 additions & 0 deletions resources/lang/en/authorization.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@
],
],

'room' => [
'destroy' => [
'not_owner' => 'Only room owner can close it.',
],
],

'score' => [
'pin' => [
'disabled_type' => "Can't pin this type of score",
Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@
});
});

Route::apiResource('rooms', 'Multiplayer\RoomsController', ['only' => ['index', 'show', 'store']]);
Route::apiResource('rooms', 'Multiplayer\RoomsController', ['only' => ['index', 'show', 'store', 'destroy']]);

Route::apiResource('seasonal-backgrounds', 'SeasonalBackgroundsController', ['only' => ['index']]);

Expand Down
88 changes: 88 additions & 0 deletions tests/Controllers/Multiplayer/RoomsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,94 @@ public function testJoinWithPassword()
$this->assertSame($initialUserChannelCount + 1, UserChannel::count());
}

public function testDestroy()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here
$url = route('api.rooms.destroy', ['room' => $room]);

$this->actAsScopedUser($room->host);
$this
->delete($url)
->assertSuccessful();

$room->refresh();
$this->assertLessThan($end, $room->ends_at);
}

public function testDestroyCannotBeCalledOnRealtimeRoom()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::REALTIME_DEFAULT_TYPE,
]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here
$url = route('api.rooms.destroy', ['room' => $room]);

$this->actAsScopedUser($room->host);
$this
->delete($url)
->assertStatus(422);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public function testDestroyCannotBeCalledByAnotherUser()
{
$requester = User::factory()->create();
$owner = User::factory()->create();
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'user_id' => $owner->getKey(),
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$url = route('api.rooms.destroy', ['room' => $room]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here

$this->actAsScopedUser($requester);
$this
->delete($url)
->assertStatus(403);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public function testDestroyCannotBeCalledAfterGracePeriod()
{
$start = now();
$end = $start->clone()->addMinutes(60);
$room = Room::factory()->create([
'starts_at' => $start,
'ends_at' => $end,
'type' => Room::PLAYLIST_TYPE,
]);
$url = route('api.rooms.destroy', ['room' => $room]);
$end = $room->ends_at; // assignment truncates fractional second part, so refetch here

$this->actAsScopedUser($room->host);
$this->travelTo($start->addMinutes(6));
$this
->delete($url)
->assertStatus(422);

$room->refresh();
$this->assertEquals($end, $room->ends_at);
}

public static function dataProviderForTestStoreWithInvalidPlayableMods(): array
{
$ret = [];
Expand Down
13 changes: 13 additions & 0 deletions tests/api_routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,19 @@
"public"
]
},
{
"uri": "api/v2/rooms/{room}",
"methods": [
"DELETE"
],
"controller": "App\\Http\\Controllers\\Multiplayer\\RoomsController@destroy",
"middlewares": [
"App\\Http\\Middleware\\ThrottleRequests:1200,1,api:",
"App\\Http\\Middleware\\RequireScopes",
"Illuminate\\Auth\\Middleware\\Authenticate"
],
"scopes": []
},
{
"uri": "api/v2/seasonal-backgrounds",
"methods": [
Expand Down

0 comments on commit 65ca10d

Please sign in to comment.