Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into add-remove-friend
Browse files Browse the repository at this point in the history
  • Loading branch information
nanaya committed Oct 29, 2024
2 parents 86d1cb5 + 87bf4e5 commit c9e7ad6
Show file tree
Hide file tree
Showing 29 changed files with 344 additions and 121 deletions.
22 changes: 12 additions & 10 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ Note that older versions of Elasticsearch do not work on ARM-based CPUs.

`osu-elastic-indexer` currently cannot update indices using Elasticsearch 7; existing records can still be queried normally.

### Mysql
### MySQL

The Mysql images provided by Docker and Mysql have different uids for the `mysql` user, if you are getting permission errors when starting the `db` container like
The MySQL images provided by Docker and MySQL have different uids for the `mysql` user, if you are getting permission errors when starting the `db` container like

mysqld: File './binlog.index' not found (OS errno 13 - Permission denied)

Expand Down Expand Up @@ -208,6 +208,14 @@ p artisan tinker

# Development

## Reset the database + seeding sample data

```
php artisan migrate:fresh --seed
```

Run the above command to rebuild the database and populate it with sample data. In order for the seeder to seed beatmaps, you must enter a valid osu! API key as the value of the `OSU_API_KEY` property in the `.env` configuration file, as the seeder obtains beatmap data from the osu! API. The key can be obtained from [the "Legacy API" section of your account settings page](https://osu.ppy.sh/home/account/edit#legacy-api).

## Creating your initial user

In the repository directory:
Expand All @@ -217,6 +225,8 @@ php artisan tinker
>>> (new App\Libraries\UserRegistration(["username" => "yourusername", "user_email" => "[email protected]", "password" => "yourpassword"]))->save();
```

Note that seeding sample data (the step above this) is required for user registration to work, otherwise the command above will fail due to missing user groups or otherwise.

## Generating assets

```bash
Expand All @@ -226,14 +236,6 @@ yarn run development

Note that if you use the bundled docker compose setup, yarn/webpack will be already run in watch mode.

## Reset the database + seeding sample data

```
php artisan migrate:fresh --seed
```

Run the above command to rebuild the database and populate it with sample data. In order for the seeder to seed beatmaps, you must enter a valid osu! API key as the value of the `OSU_API_KEY` property in the `.env` configuration file, as the seeder obtains beatmap data from the osu! API. The key can be obtained from [the "Legacy API" section of your account settings page](https://osu.ppy.sh/home/account/edit#legacy-api).

## Continuous asset generation while developing

To continuously generate assets as you make changes to files (less, coffeescript) you can run `webpack` in `watch` mode.
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/BeatmapsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public function attributes($id)
$rulesetId = $beatmap->playmode;
} else {
abort_if(
$rulesetId !== $beatmap->playmode && !$beatmap->canBeConverted(),
!$beatmap->canBeConvertedTo($rulesetId),
422,
"specified beatmap can't be converted to the specified ruleset"
);
Expand Down
32 changes: 21 additions & 11 deletions app/Http/Controllers/FriendsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,34 @@ public function index()
$currentUser = \Auth::user();
$currentMode = default_mode();

$friends = $currentUser
->friends()
->with('statistics'.studly_case($currentMode))
->eagerloadForListing()
->orderBy('username', 'asc')
->get();
$relationFriends = $currentUser->relationFriends->sortBy('username');
$relationFriends->load(array_map(
fn ($userPreload) => "target.{$userPreload}",
UserCompactTransformer::listIncludesPreload($currentMode),
));

$isApi = is_api_request();

if ($isApi && api_version() >= 20241022) {
return json_collection($relationFriends, new UserRelationTransformer(), [
"target:ruleset({$currentMode})",
...array_map(
fn ($userInclude) => "target.{$userInclude}",
UserCompactTransformer::LIST_INCLUDES,
),
]);
}

$friends = $relationFriends->pluck('target');
$usersJson = json_collection(
$friends,
(new UserCompactTransformer())->setMode($currentMode),
UserCompactTransformer::LIST_INCLUDES
);

if (is_api_request()) {
return $usersJson;
}

return ext_view('friends.index', compact('usersJson'));
return $isApi
? $usersJson
: ext_view('friends.index', compact('usersJson'));
}

public function store()
Expand Down
3 changes: 1 addition & 2 deletions app/Http/Controllers/GroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ public function show($id)

$currentMode = default_mode();
$users = $group->users()
->with('statistics'.studly_case($currentMode))
->eagerloadForListing()
->with(UserCompactTransformer::listIncludesPreload($currentMode))
->default()
->orderBy('username', 'asc')
->get();
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/ScoreTokensController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function store($beatmapId)

$checks = [
'beatmap_hash' => fn (string $value): bool => $value === $beatmap->checksum,
'ruleset_id' => fn (int $value): bool => Beatmap::modeStr($value) !== null,
'ruleset_id' => fn (int $value): bool => Beatmap::modeStr($value) !== null && $beatmap->canBeConvertedTo($value),
];
foreach ($checks as $key => $testFn) {
if (!isset($params[$key])) {
Expand Down
61 changes: 28 additions & 33 deletions app/Libraries/Search/BeatmapsetQueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace App\Libraries\Search;

use App\Models\Beatmapset;
use Carbon\Carbon;
use Carbon\CarbonImmutable;

class BeatmapsetQueryParser
{
Expand Down Expand Up @@ -114,43 +114,38 @@ private static function makeDateRangeOption(string $operator, string $value): ?a
$value = presence(trim($value, '"'));

if (preg_match('#^\d{4}$#', $value) === 1) {
$startTime = Carbon::create($value, 1, 1, 0, 0, 0, 'UTC');
$endTimeFunction = 'addYears';
$startTime = CarbonImmutable::create($value, 1, 1, 0, 0, 0, 'UTC');
$endTime = $startTime->addYears(1);
} elseif (preg_match('#^(?<year>\d{4})[-./]?(?<month>\d{1,2})$#', $value, $m) === 1) {
$startTime = Carbon::create($m['year'], $m['month'], 1, 0, 0, 0, 'UTC');
$endTimeFunction = 'addMonths';
$startTime = CarbonImmutable::create($m['year'], $m['month'], 1, 0, 0, 0, 'UTC');
$endTime = $startTime->addMonths(1);
} elseif (preg_match('#^(?<year>\d{4})[-./]?(?<month>\d{1,2})[-./]?(?<day>\d{1,2})$#', $value, $m) === 1) {
$startTime = Carbon::create($m['year'], $m['month'], $m['day'], 0, 0, 0, 'UTC');
$endTimeFunction = 'addDays';
$startTime = CarbonImmutable::create($m['year'], $m['month'], $m['day'], 0, 0, 0, 'UTC');
$endTime = $startTime->addDays(1);
} else {
$startTime = parse_time_to_carbon($value)?->utc();
$endTimeFunction = 'addSeconds';
$startTime = parse_time_to_carbon($value)?->toImmutable()->utc();
$endTime = $startTime?->addSeconds(1);
}

if (isset($startTime) && isset($endTimeFunction)) {
switch ($operator) {
case '=':
return [
'gte' => json_time($startTime),
'lt' => json_time($startTime->$endTimeFunction()),
];
case '<':
return [
'lt' => json_time($startTime),
];
case '<=':
return [
'lt' => json_time($startTime->$endTimeFunction()),
];
case '>':
return [
'gte' => json_time($startTime->$endTimeFunction()),
];
case '>=':
return [
'gte' => json_time($startTime),
];
}
if (isset($startTime) && isset($endTime)) {
return match ($operator) {
'=' => [
'gte' => $startTime->getTimestampMs(),
'lt' => $endTime->getTimestampMs(),
],
'<' => [
'lt' => $startTime->getTimestampMs(),
],
'<=' => [
'lt' => $endTime->getTimestampMs(),
],
'>' => [
'gte' => $endTime->getTimestampMs(),
],
'>=' => [
'gte' => $startTime->getTimestampMs(),
],
};
}

return null;
Expand Down
4 changes: 2 additions & 2 deletions app/Models/Beatmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ public function isScoreable()
return $this->approved > 0;
}

public function canBeConverted()
public function canBeConvertedTo(int $rulesetId)
{
return $this->playmode === static::MODES['osu'];
return $this->playmode === static::MODES['osu'] || $this->playmode === $rulesetId;
}

public function getAttribute($key)
Expand Down
2 changes: 1 addition & 1 deletion app/Models/DailyChallengeUserStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ private function updatePercentile(

foreach ($playlistPercentile as $p => $totalScore) {
if ($highScore->total_score >= $totalScore) {
$this->{"top_{$p}_placements"}++;
$this->{"{$p}_placements"}++;
}
}
$this->last_percentile_calculation = $startTime;
Expand Down
26 changes: 15 additions & 11 deletions app/Models/Multiplayer/PlaylistItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

namespace App\Models\Multiplayer;

use App\Enums\Ruleset;
use App\Exceptions\InvariantException;
use App\Models\Beatmap;
use App\Models\Model;
Expand Down Expand Up @@ -110,16 +109,14 @@ public function scoreTokens(): HasMany

public function save(array $options = [])
{
$this->assertValidMaxAttempts();
$this->validateRuleset();
$this->assertValidMods();
$this->assertValid();

return parent::save($options);
}

public function scorePercentile(): array
{
$key = "playlist_item_score_percentile:{$this->getKey()}";
$key = "playlist_item_score_percentile:v2:{$this->getKey()}";

if (!$this->expired && !$this->room->hasEnded()) {
$key .= ':ongoing';
Expand All @@ -134,15 +131,22 @@ public function scorePercentile(): array

return $count === 0
? [
'10p' => 0,
'50p' => 0,
'top_10p' => 0,
'top_50p' => 0,
] : [
'10p' => $scores[max(0, (int) ($count * 0.1) - 1)],
'50p' => $scores[max(0, (int) ($count * 0.5) - 1)],
'top_10p' => $scores[max(0, (int) ($count * 0.1) - 1)],
'top_50p' => $scores[max(0, (int) ($count * 0.5) - 1)],
];
});
}

public function assertValid()
{
$this->assertValidMaxAttempts();
$this->assertValidRuleset();
$this->assertValidMods();
}

private function assertValidMaxAttempts()
{
if ($this->max_attempts === null) {
Expand All @@ -155,10 +159,10 @@ private function assertValidMaxAttempts()
}
}

private function validateRuleset()
private function assertValidRuleset()
{
// osu beatmaps can be played in any mode, but non-osu maps can only be played in their specific modes
if ($this->beatmap->playmode !== Ruleset::osu->value && $this->beatmap->playmode !== $this->ruleset_id) {
if (!$this->beatmap->canBeConvertedTo($this->ruleset_id)) {
throw new InvariantException("invalid ruleset_id for beatmap {$this->beatmap->beatmap_id}");
}
}
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -734,5 +734,10 @@ private function assertValidStartPlay(User $user, PlaylistItem $playlistItem)
if ($playlistItem->played_at !== null) {
throw new InvariantException('Cannot play a playlist item that has already been played.');
}

// ensure the playlist item itself is in a valid state.
// this is a defensive measure to prevent further breakage if the item's state is inconsistent
// due to an external modification from osu-server-spectator.
$playlistItem->assertValid();
}
}
16 changes: 6 additions & 10 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,7 @@ public function getAttribute($key)
'rankHighests',
'rankHistories',
'receivedKudosu',
'relationFriends',
'relations',
'replaysWatchedCounts',
'reportedIn',
Expand Down Expand Up @@ -1518,6 +1519,11 @@ public function usernameChangeHistoryPublic()
->orderBy('timestamp', 'ASC');
}

public function relationFriends(): HasMany
{
return $this->relations()->friends()->withMutual();
}

public function relations()
{
return $this->hasMany(UserRelation::class);
Expand Down Expand Up @@ -2012,16 +2018,6 @@ public function scopeOnline($query)
->where('user_lastvisit', '>', time() - $GLOBALS['cfg']['osu']['user']['online_window']);
}

public function scopeEagerloadForListing($query)
{
return $query->with([
'country',
'supporterTagPurchases',
'userGroups',
'userProfileCustomization',
]);
}

public function checkPassword($password)
{
return Hash::check($password, $this->getAuthPassword());
Expand Down
2 changes: 1 addition & 1 deletion app/Models/UserRelation.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function scopeWithMutual($query)
{
$selfJoin =
'COALESCE((
SELECT 1
SELECT phpbb_zebra.friend
FROM phpbb_zebra z
WHERE phpbb_zebra.zebra_id = z.user_id
AND z.zebra_id = phpbb_zebra.user_id
Expand Down
12 changes: 11 additions & 1 deletion app/Transformers/UserCompactTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UserCompactTransformer extends TransformerAbstract
'userGroups',
];

// Paired with static::listIncludesPreload
const LIST_INCLUDES = [
...self::CARD_INCLUDES,
'statistics',
Expand Down Expand Up @@ -112,6 +113,15 @@ class UserCompactTransformer extends TransformerAbstract
'is_silenced' => 'IsNotOAuth',
];

public static function listIncludesPreload(string $rulesetName): array
{
return [
...static::CARD_INCLUDES_PRELOAD,
User::statisticsRelationName($rulesetName),
'supporterTagPurchases',
];
}

public function transform(User $user)
{
return [
Expand Down Expand Up @@ -245,7 +255,7 @@ public function includeFollowerCount(User $user)
public function includeFriends(User $user)
{
return $this->collection(
$user->relations()->friends()->withMutual()->get(),
$user->relationFriends,
new UserRelationTransformer()
);
}
Expand Down
Loading

0 comments on commit c9e7ad6

Please sign in to comment.