Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into mp-isnew
Browse files Browse the repository at this point in the history
  • Loading branch information
nanaya committed Jul 28, 2023
2 parents cc453b2 + 2a57abb commit 4997cfe
Show file tree
Hide file tree
Showing 39 changed files with 777 additions and 159 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ SLACK_ENDPOINT=https://myconan.net/null/
# S3_BASE_URL=
# S3_MINI_URL=

# S3_SOLO_REPLAY_BUCKET=solo-scores-replays

# S3_AVATAR_KEY=
# S3_AVATAR_SECRET=
# S3_AVATAR_REGION=
Expand Down Expand Up @@ -318,3 +320,6 @@ CLIENT_CHECK_VERSION=false
# OSU_URL_LAZER_WINDOWS_X64='https://github.com/ppy/osu/releases/latest/download/install.exe'
# OSU_URL_LAZER_INFO=
# OSU_URL_USER_RESTRICTION=/wiki/Help_centre/Account_restrictions

# USER_COUNTRY_CHANGE_MAX_MIXED_MONTHS=2
# USER_COUNTRY_CHANGE_MIN_MONTHS=6
2 changes: 1 addition & 1 deletion .github/workflows/pack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
run: |
export TITLE="Pending osu-web $PRODUCTION_TRACK Deployment: $GITHUB_REF_NAME"
export URL="https://github.com/ppy/osu-web/actions/runs/$GITHUB_RUN_ID"
export DESCRIPTION="Docker image was built for tag $GITHUB_REF_NAME and awaiting approval for production deployment:
export DESCRIPTION="Docker image was built for tag $GITHUB_REF_NAME and awaiting approval for $PRODUCTION_TRACK deployment:
[View Workflow Run]($URL)"
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
Expand Down
18 changes: 18 additions & 0 deletions app/Http/Controllers/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

use App\Exceptions\ImageProcessorException;
use App\Exceptions\ModelNotSavedException;
use App\Libraries\User\CountryChange;
use App\Libraries\User\CountryChangeTarget;
use App\Libraries\UserVerification;
use App\Libraries\UserVerificationState;
use App\Mail\UserEmailUpdated;
Expand Down Expand Up @@ -40,6 +42,7 @@ public function __construct()
'except' => [
'edit',
'reissueCode',
'updateCountry',
'updateEmail',
'updateNotificationOptions',
'updateOptions',
Expand Down Expand Up @@ -156,6 +159,21 @@ public function update()
return json_item($user, new CurrentUserTransformer());
}

public function updateCountry()
{
$newCountry = get_string(Request::input('country_acronym'));
$user = Auth::user();

if (CountryChangeTarget::get($user) !== $newCountry) {
abort(403, 'specified country_acronym is not allowed');
}

CountryChange::handle($user, $newCountry, 'account settings');
\Session::flash('popup', osu_trans('common.saved'));

return ext_view('layout.ujs-reload', [], 'js');
}

public function updateEmail()
{
$params = get_params(request()->all(), 'user', ['current_password', 'user_email', 'user_email_confirmation']);
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Controllers/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,10 @@ public function storeWeb()
* |------------ | -----
* | favourite | |
* | graveyard | |
* | guest | |
* | loved | |
* | most_played | |
* | nominated | |
* | pending | Previously `unranked`
* | ranked | Previously `ranked_and_approved`
*
Expand Down
21 changes: 20 additions & 1 deletion app/Libraries/Search/BeatmapsetQueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static function parse(?string $query): array
$option = static::makeIntRangeOption($op, $m['value']);
break;
case 'status':
$option = static::makeIntRangeOption($op, Beatmapset::STATES[$m['value']] ?? null);
$option = static::makeIntRangeOption($op, static::statePrefixSearch($m['value']));
break;
case 'creator':
$option = static::makeTextOption($op, $m['value']);
Expand Down Expand Up @@ -224,4 +224,23 @@ private static function makeTextOption($operator, $value)
return presence(trim($value, '"'));
}
}

private static function statePrefixSearch($value): ?int
{
if (!present($value)) {
return null;
}

if (isset(Beatmapset::STATES[$value])) {
return Beatmapset::STATES[$value];
}

foreach (Beatmapset::STATES as $string => $int) {
if (starts_with($string, $value)) {
return $int;
}
}

return null;
}
}
2 changes: 2 additions & 0 deletions app/Libraries/Session/Store.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ public function currentUserSessions()

$userId = Auth::user()->user_id;

// prevent the following save from clearing up current flash data
$this->reflash();
// flush the current session data to redis early, otherwise we will get stale metadata for the current session
$this->save();

Expand Down
46 changes: 46 additions & 0 deletions app/Libraries/User/CountryChange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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\User;

use App\Exceptions\InvariantException;
use App\Models\Beatmap;
use App\Models\Country;
use App\Models\User;
use App\Models\UserAccountHistory;

class CountryChange
{
public static function handle(User $user, string $newCountry, string $reason): void
{
// Assert valid country acronym
$country = Country::find($newCountry);
if ($country === null) {
throw new InvariantException('invalid country specified');
}

if ($user->country_acronym === $newCountry) {
return;
}

$user->getConnection()->transaction(function () use ($newCountry, $reason, $user) {
$oldCountry = $user->country_acronym;
$user->update(['country_acronym' => $newCountry]);
foreach (Beatmap::MODES as $ruleset => $_rulesetId) {
$user->statistics($ruleset, true)->update(['country_acronym' => $newCountry]);
$user->scoresBest($ruleset, true)->update(['country_acronym' => $newCountry]);
}
UserAccountHistory::addNote($user, "Changing country from {$oldCountry} to {$newCountry} ({$reason})");
});

\Artisan::queue('es:index-scores:queue', [
'--all' => true,
'--no-interaction' => true,
'--user' => $user->getKey(),
]);
}
}
106 changes: 106 additions & 0 deletions app/Libraries/User/CountryChangeTarget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?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\User;

use App\Models\Tournament;
use App\Models\TournamentRegistration;
use App\Models\User;
use App\Models\UserCountryHistory;
use Carbon\CarbonImmutable;

class CountryChangeTarget
{
const MIN_DAYS_MONTH = 15;

public static function currentMonth(): CarbonImmutable
{
$now = CarbonImmutable::now();
$subMonths = $now->day > static::MIN_DAYS_MONTH ? 0 : 1;

return $now->startOfMonth()->subMonths($subMonths);
}

public static function get(User $user): ?string
{
$minMonths = static::minMonths();
$now = CarbonImmutable::now();
$until = static::currentMonth();
$since = $until->subMonths($minMonths - 1);

if (static::isUserInTournament($user)) {
return null;
}

$history = $user
->userCountryHistory()
->whereBetween('year_month', [
UserCountryHistory::formatDate($since),
UserCountryHistory::formatDate($until),
])->whereHas('country')
->get();

// First group countries by year_month
$byMonth = [];
foreach ($history as $entry) {
$byMonth[$entry->year_month] ??= [];
$byMonth[$entry->year_month][] = $entry->country_acronym;
}

// For each year_month, summarise each countries
$byCountry = [];
foreach ($byMonth as $countries) {
$mixed = count($countries) > 1;
foreach ($countries as $country) {
$byCountry[$country] ??= [
'total' => 0,
'mixed' => 0,
];
$byCountry[$country]['total']++;
if ($mixed) {
$byCountry[$country]['mixed']++;
}
}
}

// Finally find the first country which fulfills the requirement
foreach ($byCountry as $country => $data) {
if ($data['total'] === $minMonths && $data['mixed'] <= static::maxMixedMonths()) {
if ($user->country_acronym === $country) {
return null;
} else {
return $country;
}
}
}

return null;
}

public static function maxMixedMonths(): int
{
return config('osu.user.country_change.max_mixed_months');
}

public static function minMonths(): int
{
return config('osu.user.country_change.min_months');
}

private static function isUserInTournament(User $user): bool
{
return TournamentRegistration
::where('user_id', $user->getKey())
->whereIn(
'tournament_id',
Tournament
::where('end_date', '>', CarbonImmutable::now())
->orWhereNull('end_date')
->select('tournament_id'),
)->exists();
}
}
2 changes: 1 addition & 1 deletion app/Libraries/UserRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public function save()

$this->user->setDefaultGroup($this->group);

Count::totalUsers()->increment('count');
Count::totalUsers()->incrementInstance('count');
Datadog::increment('osu.new_account_registrations', 1, ['source' => 'osu-web']);
});
} catch (Exception $e) {
Expand Down
2 changes: 1 addition & 1 deletion app/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ public function save(array $options = [])
return $this->getConnection()->transaction(function () use ($options) {
if (!$this->exists && $this->parent_id !== null && $this->parent !== null) {
// skips validation and everything
$this->parent->increment('replies_count_cache');
$this->parent->incrementInstance('replies_count_cache');
}

if ($this->isDirty('deleted_at')) {
Expand Down
2 changes: 1 addition & 1 deletion app/Models/Forum/Topic.php
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ public function markRead($user, $markTime)
throw $ex;
}

$this->increment('topic_views');
$this->incrementInstance('topic_views');
} elseif ($status->mark_time < $markTime) {
$status->update(['mark_time' => $markTime]);
}
Expand Down
24 changes: 24 additions & 0 deletions app/Models/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,37 @@ public function scopeWithPresent($query, $column)
$query->whereNotNull($column)->where($column, '<>', '');
}

/**
* Just like decrement but only works on saved instance instead of falling back to entire model
*/
public function decrementInstance()
{
if (!$this->exists) {
return false;
}

return $this->decrement(...func_get_args());
}

public function delete()
{
return $this->runAfterCommitWrapper(function () {
return parent::delete();
});
}

/**
* Just like increment but only works on saved instance instead of falling back to entire model
*/
public function incrementInstance()
{
if (!$this->exists) {
return false;
}

return $this->increment(...func_get_args());
}

public function save(array $options = [])
{
return $this->runAfterCommitWrapper(function () use ($options) {
Expand Down
7 changes: 1 addition & 6 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,12 +598,7 @@ public function startPlay(User $user, PlaylistItem $playlistItem)
return $this->getConnection()->transaction(function () use ($user, $playlistItem) {
$agg = UserScoreAggregate::new($user, $this);
if ($agg->wasRecentlyCreated) {
// sanity; if the object isn't saved, laravel will increment the entire table.
if (!$this->exists) {
$this->save();
}

$this->increment('participant_count');
$this->incrementInstance('participant_count');
}

$agg->updateUserAttempts();
Expand Down
21 changes: 10 additions & 11 deletions app/Models/Multiplayer/UserScoreAggregate.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,18 @@ public static function getPlaylistItemUserHighScore(Score $score)
]);
}

public static function lookupOrDefault(User $user, Room $room): self
public static function lookupOrDefault(User $user, Room $room): static
{
$obj = static::firstOrNew([
'user_id' => $user->getKey(),
return static::firstOrNew([
'room_id' => $room->getKey(),
'user_id' => $user->getKey(),
], [
'accuracy' => 0,
'attempts' => 0,
'completed' => 0,
'pp' => 0,
'total_score' => 0,
]);

foreach (['total_score', 'accuracy', 'pp', 'attempts', 'completed'] as $key) {
// init if required
$obj->$key = $obj->$key ?? 0;
}

return $obj;
}

public static function updatePlaylistItemUserHighScore(PlaylistItemUserHighScore $highScore, Score $score)
Expand Down Expand Up @@ -172,7 +171,7 @@ public function scopeForRanking($query)

public function updateUserAttempts()
{
$this->increment('attempts');
$this->incrementInstance('attempts');
}

public function user()
Expand Down
Loading

0 comments on commit 4997cfe

Please sign in to comment.