From 5bacdc6a95676eb20300ce65ebb61caa08377a1e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Dec 2024 15:36:51 +0900 Subject: [PATCH 01/78] Add `beatmapset_id` to multiplayer playlist items --- app/Models/Multiplayer/PlaylistItem.php | 2 ++ .../Multiplayer/PlaylistItemTransformer.php | 1 + ...apset_id_to_multiplayer_playlist_items.php | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php diff --git a/app/Models/Multiplayer/PlaylistItem.php b/app/Models/Multiplayer/PlaylistItem.php index 4c8e6175505..73d70ae77ba 100644 --- a/app/Models/Multiplayer/PlaylistItem.php +++ b/app/Models/Multiplayer/PlaylistItem.php @@ -16,6 +16,7 @@ * @property json|null $allowed_mods * @property Beatmap $beatmap * @property int $beatmap_id + * @property int|null $beatmapset_id * @property \Carbon\Carbon|null $created_at * @property int $id * @property int $owner_id @@ -64,6 +65,7 @@ public static function fromJsonParams(User $owner, $json) $obj->$field = $value; } + $obj->beatmapset_id = get_int($json['beatmapset_id'] ?? null); $obj->max_attempts = get_int($json['max_attempts'] ?? null); $modsHelper = app('mods'); diff --git a/app/Transformers/Multiplayer/PlaylistItemTransformer.php b/app/Transformers/Multiplayer/PlaylistItemTransformer.php index 99cd7f98d83..7caf2ab77ab 100644 --- a/app/Transformers/Multiplayer/PlaylistItemTransformer.php +++ b/app/Transformers/Multiplayer/PlaylistItemTransformer.php @@ -21,6 +21,7 @@ public function transform(PlaylistItem $item) 'id' => $item->id, 'room_id' => $item->room_id, 'beatmap_id' => $item->beatmap_id, + 'beatmapset_id' => $item->beatmapset_id, 'ruleset_id' => $item->ruleset_id, 'allowed_mods' => $item->allowed_mods, 'required_mods' => $item->required_mods, diff --git a/database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php b/database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php new file mode 100644 index 00000000000..70106407a55 --- /dev/null +++ b/database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php @@ -0,0 +1,29 @@ +unsignedMediumInteger('beatmapset_id')->nullable()->after('beatmap_id'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('multiplayer_playlist_items', function (Blueprint $table) { + $table->dropColumn('beatmapset_id'); + }); + } +}; From 545c945125919800fc4866d992b6d2d6fc954b29 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 10 Dec 2024 21:34:51 +0900 Subject: [PATCH 02/78] render user tags --- .../Controllers/BeatmapsetsController.php | 1 + .../BeatmapsetCompactTransformer.php | 8 ++++++++ resources/js/beatmapsets-show/info.tsx | 19 ++++++++++++------- .../js/interfaces/beatmapset-extended-json.ts | 1 + resources/js/interfaces/beatmapset-json.ts | 2 ++ resources/js/interfaces/tag-json.ts | 8 ++++++++ 6 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 resources/js/interfaces/tag-json.ts diff --git a/app/Http/Controllers/BeatmapsetsController.php b/app/Http/Controllers/BeatmapsetsController.php index d0958f9567a..d8f91d7b5ad 100644 --- a/app/Http/Controllers/BeatmapsetsController.php +++ b/app/Http/Controllers/BeatmapsetsController.php @@ -417,6 +417,7 @@ private function showJson($beatmapset) 'recent_favourites', 'related_users', 'user', + 'user_tags', ]); } } diff --git a/app/Transformers/BeatmapsetCompactTransformer.php b/app/Transformers/BeatmapsetCompactTransformer.php index 546fe4125fe..fd3f7d0694c 100644 --- a/app/Transformers/BeatmapsetCompactTransformer.php +++ b/app/Transformers/BeatmapsetCompactTransformer.php @@ -41,6 +41,7 @@ class BeatmapsetCompactTransformer extends TransformerAbstract 'recent_favourites', 'related_users', 'user', + 'user_tags', ]; // TODO: switch to enum after php 8.1 @@ -299,6 +300,13 @@ public function includeRelatedUsers(Beatmapset $beatmapset) return $this->collection($users, new UserCompactTransformer()); } + public function includeUserTags(Beatmapset $beatmapset) + { + $beatmaps = $this->beatmaps($beatmapset); + + return $this->collection($beatmaps->flatMap->topTags(), new TagTransformer()); + } + private function beatmaps(Beatmapset $beatmapset, ?Fractal\ParamBag $params = null): EloquentCollection { $rel = $beatmapset->trashed() || ($params !== null && $params->get('with_trashed')) ? 'allBeatmaps' : 'beatmaps'; diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index a90cdb9e042..e1974cbda50 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -65,6 +65,15 @@ export default class Info extends React.Component { return ret; } + private get tags() { + return [ + ...this.controller.beatmapset.user_tags.map((tag) => tag.name), + ...this.controller.beatmapset.tags + .split(' ') + .filter(present), + ]; + } + private get withEditDescription() { return this.controller.beatmapset.description.bbcode != null; } @@ -84,10 +93,6 @@ export default class Info extends React.Component { } render() { - const tags = this.controller.beatmapset.tags - .split(' ') - .filter(present); - return (
{this.isEditingDescription && @@ -191,14 +196,14 @@ export default class Info extends React.Component {
- {tags.length > 0 && + {this.tags.length > 0 &&

{trans('beatmapsets.show.info.tags')}

- {tags.map((tag, i) => ( - + {this.tags.map((tag) => ( + >; export type BeatmapsetJsonForShow = diff --git a/resources/js/interfaces/beatmapset-json.ts b/resources/js/interfaces/beatmapset-json.ts index 533ee955779..cb59fc13390 100644 --- a/resources/js/interfaces/beatmapset-json.ts +++ b/resources/js/interfaces/beatmapset-json.ts @@ -9,6 +9,7 @@ import BeatmapsetNominationJson from './beatmapset-nomination-json'; import GenreJson from './genre-json'; import LanguageJson from './language-json'; import Ruleset from './ruleset'; +import TagJson from './tag-json'; import UserJson, { UserJsonDeleted } from './user-json'; export interface Availability { @@ -94,6 +95,7 @@ interface BeatmapsetJsonAvailableIncludes { recent_favourites: UserJson[]; related_users: UserJson[]; user: UserJson | UserJsonDeleted; + user_tags: TagJson[]; } interface HypeData { diff --git a/resources/js/interfaces/tag-json.ts b/resources/js/interfaces/tag-json.ts new file mode 100644 index 00000000000..5d1352a7a4b --- /dev/null +++ b/resources/js/interfaces/tag-json.ts @@ -0,0 +1,8 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +export default interface TagJson { + description: string; + id: number; + name: string; +} From 4ae47f9f743c928a7f6412f395f7eaba35056b12 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 12 Dec 2024 15:57:19 +0900 Subject: [PATCH 03/78] cache tags locally, only cache beatmap tags with ids and counts; return description with response? --- .../Controllers/BeatmapTagsController.php | 9 +---- app/Http/Controllers/TagsController.php | 9 +---- app/Models/Beatmap.php | 26 ++++++++++++++ app/Models/Tag.php | 6 ++-- app/Providers/AppServiceProvider.php | 1 + app/Singletons/Tags.php | 35 +++++++++++++++++++ 6 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 app/Singletons/Tags.php diff --git a/app/Http/Controllers/BeatmapTagsController.php b/app/Http/Controllers/BeatmapTagsController.php index 87c9d9b253b..630010f7564 100644 --- a/app/Http/Controllers/BeatmapTagsController.php +++ b/app/Http/Controllers/BeatmapTagsController.php @@ -29,15 +29,8 @@ public function __construct() public function index($beatmapId) { - $topBeatmapTags = cache_remember_mutexed( - "beatmap_tags:{$beatmapId}", - $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], - [], - fn () => Tag::topTags($beatmapId), - ); - return [ - 'beatmap_tags' => $topBeatmapTags, + 'beatmap_tags' => Beatmap::findOrFail($beatmapId)->topTagsJson(), ]; } diff --git a/app/Http/Controllers/TagsController.php b/app/Http/Controllers/TagsController.php index 80b657d9182..a93ddeff028 100644 --- a/app/Http/Controllers/TagsController.php +++ b/app/Http/Controllers/TagsController.php @@ -21,15 +21,8 @@ public function __construct() public function index() { - $tags = cache_remember_mutexed( - 'tags', - $GLOBALS['cfg']['osu']['tags']['tags_cache_duration'], - [], - fn () => Tag::all(), - ); - return [ - 'tags' => json_collection($tags, new TagTransformer()), + 'tags' => app('tags')->json(), ]; } } diff --git a/app/Models/Beatmap.php b/app/Models/Beatmap.php index b107d638d17..a042eebcf9c 100644 --- a/app/Models/Beatmap.php +++ b/app/Models/Beatmap.php @@ -348,6 +348,32 @@ public function status() return array_search($this->approved, Beatmapset::STATES, true); } + public function topTagsJson() + { + $tagIds = cache_remember_mutexed( + "beatmap_top_tag_ids:{$this->getKey()}", + $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], + [], + fn () => Tag::topTagIds($this->getKey())->toArray(), + ); + + $cachedTags = app('tags'); + $json = []; + + foreach ($tagIds as $tagId) { + $tag = $cachedTags->get($tagId['id']); + if ($tag !== null) { + $json[] = [ + ...$tagId, + 'description' => $tag->description, + 'name' => $tag->name, + ]; + } + } + + return $json; + } + private function getDifficultyrating() { if ($this->convert) { diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 3513ab3d891..74293c5c00c 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -23,7 +23,7 @@ public function beatmapTags(): HasMany return $this->hasMany(BeatmapTag::class); } - public static function topTags($beatmapId) + public static function topTagIds(int $beatmapId, int $limit = 50) { return static ::joinRelation( @@ -31,11 +31,11 @@ public static function topTags($beatmapId) fn ($q) => $q->where('beatmap_id', $beatmapId)->whereHas('user', fn ($userQuery) => $userQuery->default()) ) ->groupBy('id') - ->select('id', 'name') + ->select('id') ->selectRaw('COUNT(*) as count') ->orderBy('count', 'desc') ->orderBy('id', 'desc') - ->limit(50) + ->limit($limit) ->get(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index bd080bdcae9..7d358070636 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -31,6 +31,7 @@ class AppServiceProvider extends ServiceProvider 'layout-cache' => Singletons\LayoutCache::class, 'medals' => Singletons\Medals::class, 'smilies' => Singletons\Smilies::class, + 'tags' => Singletons\Tags::class, 'user-cover-presets' => Singletons\UserCoverPresets::class, ]; diff --git a/app/Singletons/Tags.php b/app/Singletons/Tags.php new file mode 100644 index 00000000000..b8cccbd6e46 --- /dev/null +++ b/app/Singletons/Tags.php @@ -0,0 +1,35 @@ +. 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\Singletons; + +use App\Models\Tag; +use App\Traits\Memoizes; +use App\Transformers\TagTransformer; + +class Tags +{ + use Memoizes; + + public function get(int $id): ?Tag + { + $allById = $this->memoize( + 'allById', + fn () => Tag::all()->keyBy('id'), + ); + + return $allById[$id] ?? null; + } + + public function json(): array + { + return $this->memoize( + __FUNCTION__, + fn () => json_collection(Tag::all(), new TagTransformer()), + ); + } +} From 4a55d98edebb9e5041115a33e339c986d6d73076 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Fri, 13 Dec 2024 15:38:29 +0900 Subject: [PATCH 04/78] return tags with counts with beatmaps --- app/Http/Controllers/BeatmapsetsController.php | 2 +- app/Transformers/BeatmapCompactTransformer.php | 6 ++++++ resources/js/beatmapsets-show/controller.ts | 18 ++++++++++++++++++ resources/js/beatmapsets-show/info.tsx | 9 +++++++-- resources/js/interfaces/beatmap-json.ts | 2 ++ .../js/interfaces/beatmapset-extended-json.ts | 1 - resources/js/interfaces/tag-json.ts | 1 + 7 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/BeatmapsetsController.php b/app/Http/Controllers/BeatmapsetsController.php index d8f91d7b5ad..692f089fda3 100644 --- a/app/Http/Controllers/BeatmapsetsController.php +++ b/app/Http/Controllers/BeatmapsetsController.php @@ -404,6 +404,7 @@ private function showJson($beatmapset) 'beatmaps.failtimes', 'beatmaps.max_combo', 'beatmaps.owners', + 'beatmaps.tags', 'converts', 'converts.failtimes', 'converts.owners', @@ -417,7 +418,6 @@ private function showJson($beatmapset) 'recent_favourites', 'related_users', 'user', - 'user_tags', ]); } } diff --git a/app/Transformers/BeatmapCompactTransformer.php b/app/Transformers/BeatmapCompactTransformer.php index ef8a96c6542..32cad40ce36 100644 --- a/app/Transformers/BeatmapCompactTransformer.php +++ b/app/Transformers/BeatmapCompactTransformer.php @@ -18,6 +18,7 @@ class BeatmapCompactTransformer extends TransformerAbstract 'failtimes', 'max_combo', 'owners', + 'tags', 'user', ]; @@ -83,6 +84,11 @@ public function includeOwners(Beatmap $beatmap) ]); } + public function includeTags(Beatmap $beatmap) + { + return $this->primitive($beatmap->topTagsJson()); + } + public function includeUser(Beatmap $beatmap) { return $this->item( diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 7447723e6cf..54b2fb2f0ea 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +import BeatmapJson from 'interfaces/beatmap-json'; import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json'; import UserJson from 'interfaces/user-json'; import { keyBy } from 'lodash'; @@ -70,6 +71,23 @@ export default class Controller { return this.beatmaps.get(this.currentBeatmap.mode) ?? []; } + @computed + get tags() { + const summedTags: Partial> = {}; + for (const beatmap of this.beatmapset.beatmaps) { + if (beatmap.tags == null) continue; + for (const tag of beatmap.tags) { + if (summedTags[tag.id] != null) { + summedTags[tag.id].count += tag.count; + } else { + summedTags[tag.id] = tag; + } + } + } + + return summedTags; + } + @computed get usersById() { return keyBy(this.beatmapset.related_users, 'id') as Partial>; diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index e1974cbda50..97c5b1dafea 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -66,9 +66,14 @@ export default class Info extends React.Component { } private get tags() { + const sortedTags = Object.values(this.controller.tags).sort((a, b) => { + const diff = b.count - a.count; + return diff !== 0 ? diff : a.id - b.id; + }); + return [ - ...this.controller.beatmapset.user_tags.map((tag) => tag.name), - ...this.controller.beatmapset.tags + ...sortedTags.map((tag) => tag.name), + ...this.controller.beatmapset.tags // TODO: something about duplicate mapper tags .split(' ') .filter(present), ]; diff --git a/resources/js/interfaces/beatmap-json.ts b/resources/js/interfaces/beatmap-json.ts index a7d40211981..ee6db218529 100644 --- a/resources/js/interfaces/beatmap-json.ts +++ b/resources/js/interfaces/beatmap-json.ts @@ -4,6 +4,7 @@ import BeatmapOwnerJson from './beatmap-owner-json'; import BeatmapsetJson from './beatmapset-json'; import Ruleset from './ruleset'; +import TagJson from './tag-json'; import UserJson from './user-json'; interface BeatmapFailTimesArray { @@ -17,6 +18,7 @@ interface BeatmapJsonAvailableIncludes { failtimes: BeatmapFailTimesArray; max_combo: number; owners: BeatmapOwnerJson[]; + tags: (TagJson & Required>)[]; user: UserJson; } diff --git a/resources/js/interfaces/beatmapset-extended-json.ts b/resources/js/interfaces/beatmapset-extended-json.ts index 20f2f19a9e6..1a41e2c9d3c 100644 --- a/resources/js/interfaces/beatmapset-extended-json.ts +++ b/resources/js/interfaces/beatmapset-extended-json.ts @@ -57,7 +57,6 @@ type BeatmapsetJsonForShowIncludes = Required>; export type BeatmapsetJsonForShow = diff --git a/resources/js/interfaces/tag-json.ts b/resources/js/interfaces/tag-json.ts index 5d1352a7a4b..557b39d4b8b 100644 --- a/resources/js/interfaces/tag-json.ts +++ b/resources/js/interfaces/tag-json.ts @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. export default interface TagJson { + count?: number; description: string; id: number; name: string; From b3fcdfa9b1b4f2fc3c8761381aa99077ccf4e5d3 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Fri, 13 Dec 2024 16:56:04 +0900 Subject: [PATCH 05/78] add TagJsonWithCount type --- resources/js/beatmapsets-show/controller.ts | 14 ++++++++------ resources/js/interfaces/beatmap-json.ts | 4 ++-- resources/js/interfaces/tag-json.ts | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 54b2fb2f0ea..b3d1ab36681 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -1,8 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. -import BeatmapJson from 'interfaces/beatmap-json'; import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json'; +import { TagJsonWithCount } from 'interfaces/tag-json'; import UserJson from 'interfaces/user-json'; import { keyBy } from 'lodash'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; @@ -73,15 +73,17 @@ export default class Controller { @computed get tags() { - const summedTags: Partial> = {}; + const summedTags: Partial> = {}; for (const beatmap of this.beatmapset.beatmaps) { if (beatmap.tags == null) continue; + for (const tag of beatmap.tags) { - if (summedTags[tag.id] != null) { - summedTags[tag.id].count += tag.count; - } else { - summedTags[tag.id] = tag; + const summedTag = summedTags[tag.id]; + if (summedTag != null) { + summedTag.count += tag.count; } + + summedTags[tag.id] = summedTag; } } diff --git a/resources/js/interfaces/beatmap-json.ts b/resources/js/interfaces/beatmap-json.ts index ee6db218529..8400706c956 100644 --- a/resources/js/interfaces/beatmap-json.ts +++ b/resources/js/interfaces/beatmap-json.ts @@ -4,7 +4,7 @@ import BeatmapOwnerJson from './beatmap-owner-json'; import BeatmapsetJson from './beatmapset-json'; import Ruleset from './ruleset'; -import TagJson from './tag-json'; +import { TagJsonWithCount } from './tag-json'; import UserJson from './user-json'; interface BeatmapFailTimesArray { @@ -18,7 +18,7 @@ interface BeatmapJsonAvailableIncludes { failtimes: BeatmapFailTimesArray; max_combo: number; owners: BeatmapOwnerJson[]; - tags: (TagJson & Required>)[]; + tags: TagJsonWithCount[]; user: UserJson; } diff --git a/resources/js/interfaces/tag-json.ts b/resources/js/interfaces/tag-json.ts index 557b39d4b8b..da857843edc 100644 --- a/resources/js/interfaces/tag-json.ts +++ b/resources/js/interfaces/tag-json.ts @@ -7,3 +7,5 @@ export default interface TagJson { id: number; name: string; } + +export type TagJsonWithCount = TagJson & Required>; From d88307554c57a4b441deea93fa2c87e65ad8ec62 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Fri, 13 Dec 2024 18:55:35 +0900 Subject: [PATCH 06/78] merge user and mapper tags, place mapper tags at the end --- resources/js/beatmapsets-show/controller.ts | 25 ++++++++++++++++----- resources/js/beatmapsets-show/info.tsx | 11 ++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index b3d1ab36681..542d6c251b8 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -5,12 +5,13 @@ import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json'; import { TagJsonWithCount } from 'interfaces/tag-json'; import UserJson from 'interfaces/user-json'; import { keyBy } from 'lodash'; -import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction, toJS } from 'mobx'; import { deletedUserJson } from 'models/user'; import core from 'osu-core-singleton'; import { find, findDefault, group } from 'utils/beatmap-helper'; import { parse } from 'utils/beatmapset-page-hash'; import { parseJson } from 'utils/json'; +import { present } from 'utils/string'; import { currentUrl } from 'utils/turbolinks'; export type ScoreLoadingState = null | 'error' | 'loading' | 'supporter_only' | 'unranked'; @@ -73,21 +74,33 @@ export default class Controller { @computed get tags() { - const summedTags: Partial> = {}; + const mapperTagSet = new Set(this.beatmapset.tags.split(' ').filter(present)); + + const userTags: Partial> = {}; for (const beatmap of this.beatmapset.beatmaps) { if (beatmap.tags == null) continue; for (const tag of beatmap.tags) { - const summedTag = summedTags[tag.id]; - if (summedTag != null) { + let summedTag = userTags[tag.id]; + if (summedTag == null) { + summedTag = toJS(tag); // don't modify original + userTags[tag.id] = summedTag; + } else { summedTag.count += tag.count; } - summedTags[tag.id] = summedTag; + // TODO: case insensitivity + if (mapperTagSet.has(tag.name)) { + summedTag.count++; + mapperTagSet.delete(tag.name); + } } } - return summedTags; + return { + mapperTags: [...mapperTagSet.values()], + userTags, + }; } @computed diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index 97c5b1dafea..a9c539039ed 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -66,16 +66,17 @@ export default class Info extends React.Component { } private get tags() { - const sortedTags = Object.values(this.controller.tags).sort((a, b) => { + const tags = this.controller.tags; + + const sortedUserTags = Object.values(tags.userTags).sort((a, b) => { + if (a == null || b == null) return 0; // for typing only, doesn't contain nulls. const diff = b.count - a.count; return diff !== 0 ? diff : a.id - b.id; }); return [ - ...sortedTags.map((tag) => tag.name), - ...this.controller.beatmapset.tags // TODO: something about duplicate mapper tags - .split(' ') - .filter(present), + ...sortedUserTags.map((tag) => tag?.name), + ...tags.mapperTags, ]; } From 3c9a6f5fbee35823f67ee1c7e4f4488d4952a89b Mon Sep 17 00:00:00 2001 From: bakaneko Date: Fri, 13 Dec 2024 20:49:04 +0900 Subject: [PATCH 07/78] do the sorting in controller --- resources/js/beatmapsets-show/controller.ts | 12 ++++++++---- resources/js/beatmapsets-show/info.tsx | 8 +------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 542d6c251b8..05e784e6349 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -76,15 +76,15 @@ export default class Controller { get tags() { const mapperTagSet = new Set(this.beatmapset.tags.split(' ').filter(present)); - const userTags: Partial> = {}; + const tags: Partial> = {}; for (const beatmap of this.beatmapset.beatmaps) { if (beatmap.tags == null) continue; for (const tag of beatmap.tags) { - let summedTag = userTags[tag.id]; + let summedTag = tags[tag.id]; if (summedTag == null) { summedTag = toJS(tag); // don't modify original - userTags[tag.id] = summedTag; + tags[tag.id] = summedTag; } else { summedTag.count += tag.count; } @@ -99,7 +99,11 @@ export default class Controller { return { mapperTags: [...mapperTagSet.values()], - userTags, + userTags: Object.values(tags).sort((a, b) => { + if (a == null || b == null) return 0; // for typing only, doesn't contain nulls. + const diff = b.count - a.count; + return diff !== 0 ? diff : a.id - b.id; + }), }; } diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index a9c539039ed..56cbe3641fc 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -68,14 +68,8 @@ export default class Info extends React.Component { private get tags() { const tags = this.controller.tags; - const sortedUserTags = Object.values(tags.userTags).sort((a, b) => { - if (a == null || b == null) return 0; // for typing only, doesn't contain nulls. - const diff = b.count - a.count; - return diff !== 0 ? diff : a.id - b.id; - }); - return [ - ...sortedUserTags.map((tag) => tag?.name), + ...tags.userTags.map((tag) => tag?.name), ...tags.mapperTags, ]; } From 1503ceb9fe3b1080d56b23adc005a63aa93a9e22 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 17 Dec 2024 19:16:39 +0900 Subject: [PATCH 08/78] lint --- app/Http/Controllers/TagsController.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Http/Controllers/TagsController.php b/app/Http/Controllers/TagsController.php index a93ddeff028..b0ce5c493a9 100644 --- a/app/Http/Controllers/TagsController.php +++ b/app/Http/Controllers/TagsController.php @@ -7,9 +7,6 @@ namespace App\Http\Controllers; -use App\Models\Tag; -use App\Transformers\TagTransformer; - class TagsController extends Controller { public function __construct() From f5552af474b41005cec5c8e60ab54dd0772d9de6 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 17 Dec 2024 20:16:20 +0900 Subject: [PATCH 09/78] return id and counts seperately from full tag --- .../Controllers/BeatmapsetsController.php | 3 +- app/Models/Beatmap.php | 20 +-------- .../BeatmapCompactTransformer.php | 6 +-- .../BeatmapsetCompactTransformer.php | 17 ++++++-- resources/js/beatmapsets-show/controller.ts | 41 ++++++++++++------- resources/js/interfaces/beatmap-json.ts | 3 +- .../js/interfaces/beatmapset-extended-json.ts | 1 + resources/js/interfaces/beatmapset-json.ts | 1 + 8 files changed, 50 insertions(+), 42 deletions(-) diff --git a/app/Http/Controllers/BeatmapsetsController.php b/app/Http/Controllers/BeatmapsetsController.php index 692f089fda3..a2da6fc6b37 100644 --- a/app/Http/Controllers/BeatmapsetsController.php +++ b/app/Http/Controllers/BeatmapsetsController.php @@ -404,7 +404,7 @@ private function showJson($beatmapset) 'beatmaps.failtimes', 'beatmaps.max_combo', 'beatmaps.owners', - 'beatmaps.tags', + 'beatmaps.top_tag_ids', 'converts', 'converts.failtimes', 'converts.owners', @@ -416,6 +416,7 @@ private function showJson($beatmapset) 'pack_tags', 'ratings', 'recent_favourites', + 'related_tags', 'related_users', 'user', ]); diff --git a/app/Models/Beatmap.php b/app/Models/Beatmap.php index a042eebcf9c..57f18ae0aaa 100644 --- a/app/Models/Beatmap.php +++ b/app/Models/Beatmap.php @@ -348,30 +348,14 @@ public function status() return array_search($this->approved, Beatmapset::STATES, true); } - public function topTagsJson() + public function topTagIds() { - $tagIds = cache_remember_mutexed( + return cache_remember_mutexed( "beatmap_top_tag_ids:{$this->getKey()}", $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], [], fn () => Tag::topTagIds($this->getKey())->toArray(), ); - - $cachedTags = app('tags'); - $json = []; - - foreach ($tagIds as $tagId) { - $tag = $cachedTags->get($tagId['id']); - if ($tag !== null) { - $json[] = [ - ...$tagId, - 'description' => $tag->description, - 'name' => $tag->name, - ]; - } - } - - return $json; } private function getDifficultyrating() diff --git a/app/Transformers/BeatmapCompactTransformer.php b/app/Transformers/BeatmapCompactTransformer.php index 32cad40ce36..9ec4aa198c3 100644 --- a/app/Transformers/BeatmapCompactTransformer.php +++ b/app/Transformers/BeatmapCompactTransformer.php @@ -18,7 +18,7 @@ class BeatmapCompactTransformer extends TransformerAbstract 'failtimes', 'max_combo', 'owners', - 'tags', + 'top_tag_ids', 'user', ]; @@ -84,9 +84,9 @@ public function includeOwners(Beatmap $beatmap) ]); } - public function includeTags(Beatmap $beatmap) + public function includeTopTagIds(Beatmap $beatmap) { - return $this->primitive($beatmap->topTagsJson()); + return $this->primitive($beatmap->topTagIds()); } public function includeUser(Beatmap $beatmap) diff --git a/app/Transformers/BeatmapsetCompactTransformer.php b/app/Transformers/BeatmapsetCompactTransformer.php index fd3f7d0694c..59f47b44d71 100644 --- a/app/Transformers/BeatmapsetCompactTransformer.php +++ b/app/Transformers/BeatmapsetCompactTransformer.php @@ -40,8 +40,8 @@ class BeatmapsetCompactTransformer extends TransformerAbstract 'ratings', 'recent_favourites', 'related_users', + 'related_tags', 'user', - 'user_tags', ]; // TODO: switch to enum after php 8.1 @@ -300,11 +300,22 @@ public function includeRelatedUsers(Beatmapset $beatmapset) return $this->collection($users, new UserCompactTransformer()); } - public function includeUserTags(Beatmapset $beatmapset) + public function includeRelatedTags(Beatmapset $beatmapset) { $beatmaps = $this->beatmaps($beatmapset); + $tagIdSet = new Set($beatmaps->flatMap->topTagIds()->pluck('id')); - return $this->collection($beatmaps->flatMap->topTags(), new TagTransformer()); + $cachedTags = app('tags'); + $json = []; + + foreach ($tagIdSet as $tagId) { + $tag = $cachedTags->get($tagId); + if ($tag !== null) { + $json[] = $tag; + } + } + + return $this->primitive($json); } private function beatmaps(Beatmapset $beatmapset, ?Fractal\ParamBag $params = null): EloquentCollection diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 05e784e6349..e93119133f3 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -5,7 +5,7 @@ import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json'; import { TagJsonWithCount } from 'interfaces/tag-json'; import UserJson from 'interfaces/user-json'; import { keyBy } from 'lodash'; -import { action, computed, makeObservable, observable, runInAction, toJS } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { deletedUserJson } from 'models/user'; import core from 'osu-core-singleton'; import { find, findDefault, group } from 'utils/beatmap-helper'; @@ -72,26 +72,38 @@ export default class Controller { return this.beatmaps.get(this.currentBeatmap.mode) ?? []; } + get relatedTags() { + const map = new Map(); + + for (const tag of this.beatmapset.related_tags) { + map.set(tag.id, tag); + } + + return map; + } + @computed get tags() { const mapperTagSet = new Set(this.beatmapset.tags.split(' ').filter(present)); + const tagMap = new Map(); + + for (const tag of this.beatmapset.related_tags) { + tag.count = 0; // assign 0 and cast + tagMap.set(tag.id, tag as TagJsonWithCount); + } - const tags: Partial> = {}; for (const beatmap of this.beatmapset.beatmaps) { - if (beatmap.tags == null) continue; - - for (const tag of beatmap.tags) { - let summedTag = tags[tag.id]; - if (summedTag == null) { - summedTag = toJS(tag); // don't modify original - tags[tag.id] = summedTag; - } else { - summedTag.count += tag.count; - } + if (beatmap.top_tag_ids == null) continue; + + for (const tagId of beatmap.top_tag_ids) { + const tag = tagMap.get(tagId.id); + if (tag == null) continue; + + tag.count += tagId.count; // TODO: case insensitivity if (mapperTagSet.has(tag.name)) { - summedTag.count++; + tag.count++; mapperTagSet.delete(tag.name); } } @@ -99,8 +111,7 @@ export default class Controller { return { mapperTags: [...mapperTagSet.values()], - userTags: Object.values(tags).sort((a, b) => { - if (a == null || b == null) return 0; // for typing only, doesn't contain nulls. + userTags: [...tagMap.values()].sort((a, b) => { const diff = b.count - a.count; return diff !== 0 ? diff : a.id - b.id; }), diff --git a/resources/js/interfaces/beatmap-json.ts b/resources/js/interfaces/beatmap-json.ts index 8400706c956..9c5837a51ea 100644 --- a/resources/js/interfaces/beatmap-json.ts +++ b/resources/js/interfaces/beatmap-json.ts @@ -4,7 +4,6 @@ import BeatmapOwnerJson from './beatmap-owner-json'; import BeatmapsetJson from './beatmapset-json'; import Ruleset from './ruleset'; -import { TagJsonWithCount } from './tag-json'; import UserJson from './user-json'; interface BeatmapFailTimesArray { @@ -18,7 +17,7 @@ interface BeatmapJsonAvailableIncludes { failtimes: BeatmapFailTimesArray; max_combo: number; owners: BeatmapOwnerJson[]; - tags: TagJsonWithCount[]; + top_tag_ids: { count: number; id: number }[]; user: UserJson; } diff --git a/resources/js/interfaces/beatmapset-extended-json.ts b/resources/js/interfaces/beatmapset-extended-json.ts index 1a41e2c9d3c..c7ff2f4038d 100644 --- a/resources/js/interfaces/beatmapset-extended-json.ts +++ b/resources/js/interfaces/beatmapset-extended-json.ts @@ -55,6 +55,7 @@ type BeatmapsetJsonForShowIncludes = Required>; diff --git a/resources/js/interfaces/beatmapset-json.ts b/resources/js/interfaces/beatmapset-json.ts index cb59fc13390..6519a014d1d 100644 --- a/resources/js/interfaces/beatmapset-json.ts +++ b/resources/js/interfaces/beatmapset-json.ts @@ -93,6 +93,7 @@ interface BeatmapsetJsonAvailableIncludes { nominations: BeatmapsetNominationsInterface; ratings: number[]; recent_favourites: UserJson[]; + related_tags: TagJson[]; related_users: UserJson[]; user: UserJson | UserJsonDeleted; user_tags: TagJson[]; From 5ad134a1904fa3c99e615d26df84a6f23c594d6b Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 17 Dec 2024 22:11:43 +0900 Subject: [PATCH 10/78] remove route (get from beatmapset instead) --- app/Http/Controllers/BeatmapTagsController.php | 9 --------- routes/web.php | 2 +- tests/Controllers/BeatmapTagsControllerTest.php | 16 ---------------- tests/api_routes.json | 16 ---------------- 4 files changed, 1 insertion(+), 42 deletions(-) diff --git a/app/Http/Controllers/BeatmapTagsController.php b/app/Http/Controllers/BeatmapTagsController.php index 630010f7564..f6cf56b035a 100644 --- a/app/Http/Controllers/BeatmapTagsController.php +++ b/app/Http/Controllers/BeatmapTagsController.php @@ -23,15 +23,6 @@ public function __construct() 'destroy', ], ]); - - $this->middleware('require-scopes:public', ['only' => 'index']); - } - - public function index($beatmapId) - { - return [ - 'beatmap_tags' => Beatmap::findOrFail($beatmapId)->topTagsJson(), - ]; } public function destroy($beatmapId, $tagId) diff --git a/routes/web.php b/routes/web.php index a94e8853fa3..9b3bfd6151e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -430,7 +430,7 @@ }); }); - Route::apiResource('tags', 'BeatmapTagsController', ['only' => ['index', 'store', 'destroy']]); + Route::apiResource('tags', 'BeatmapTagsController', ['only' => ['store', 'destroy']]); }); }); diff --git a/tests/Controllers/BeatmapTagsControllerTest.php b/tests/Controllers/BeatmapTagsControllerTest.php index 84b17bd570f..f75438c1454 100644 --- a/tests/Controllers/BeatmapTagsControllerTest.php +++ b/tests/Controllers/BeatmapTagsControllerTest.php @@ -12,7 +12,6 @@ use App\Models\Solo\Score; use App\Models\Tag; use App\Models\User; -use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; class BeatmapTagsControllerTest extends TestCase @@ -21,21 +20,6 @@ class BeatmapTagsControllerTest extends TestCase private Beatmap $beatmap; private BeatmapTag $beatmapTag; - public function testIndex(): void - { - $this->actAsScopedUser(User::factory()->create(), ['public']); - - $this - ->get(route('api.beatmaps.tags.index', ['beatmap' => $this->beatmap->getKey()])) - ->assertSuccessful() - ->assertJson(fn (AssertableJson $json) => - $json - ->where('beatmap_tags.0.id', $this->tag->getKey()) - ->where('beatmap_tags.0.name', $this->tag->name) - ->where('beatmap_tags.0.count', 1) - ->etc()); - } - public function testStore(): void { $user = User::factory() diff --git a/tests/api_routes.json b/tests/api_routes.json index 68fe32aac33..007f537198f 100644 --- a/tests/api_routes.json +++ b/tests/api_routes.json @@ -173,22 +173,6 @@ ], "scopes": [] }, - { - "uri": "api/v2/beatmaps/{beatmap}/tags", - "methods": [ - "GET", - "HEAD" - ], - "controller": "App\\Http\\Controllers\\BeatmapTagsController@index", - "middlewares": [ - "App\\Http\\Middleware\\ThrottleRequests:1200,1,api:", - "App\\Http\\Middleware\\RequireScopes", - "App\\Http\\Middleware\\RequireScopes:public" - ], - "scopes": [ - "public" - ] - }, { "uri": "api/v2/beatmaps/{beatmap}/tags", "methods": [ From 3c9c2e51c20ec395525c7160fd8c8b0895f4b651 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 17 Dec 2024 22:19:33 +0900 Subject: [PATCH 11/78] not nullable --- resources/js/beatmapsets-show/info.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index 56cbe3641fc..b5e3eadb3e1 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -69,7 +69,7 @@ export default class Info extends React.Component { const tags = this.controller.tags; return [ - ...tags.userTags.map((tag) => tag?.name), + ...tags.userTags.map((tag) => tag.name), ...tags.mapperTags, ]; } From 9e01f3d8ba725393395677f3c5740a02e92b5362 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 18 Dec 2024 15:27:59 +0900 Subject: [PATCH 12/78] don't need the extra join/relation anymore; make into query --- app/Models/Beatmap.php | 2 +- app/Models/BeatmapTag.php | 12 ++++++++++++ app/Models/Tag.php | 16 ---------------- .../BeatmapsetCompactTransformer.php | 2 +- resources/js/beatmapsets-show/controller.ts | 2 +- resources/js/interfaces/beatmap-json.ts | 2 +- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/app/Models/Beatmap.php b/app/Models/Beatmap.php index 57f18ae0aaa..54205677699 100644 --- a/app/Models/Beatmap.php +++ b/app/Models/Beatmap.php @@ -354,7 +354,7 @@ public function topTagIds() "beatmap_top_tag_ids:{$this->getKey()}", $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], [], - fn () => Tag::topTagIds($this->getKey())->toArray(), + fn () => BeatmapTag::topTagIdsQuery($this->getKey())->get()->toArray(), ); } diff --git a/app/Models/BeatmapTag.php b/app/Models/BeatmapTag.php index a4fca351852..8b1cf46fd17 100644 --- a/app/Models/BeatmapTag.php +++ b/app/Models/BeatmapTag.php @@ -19,6 +19,18 @@ class BeatmapTag extends Model protected $primaryKey = ':composite'; protected $primaryKeys = ['beatmap_id', 'tag_id', 'user_id']; + public static function topTagIdsQuery(int $beatmapId, int $limit = 50) + { + return static::where('beatmap_id', $beatmapId) + ->whereHas('user', fn ($userQuery) => $userQuery->default()) + ->groupBy('tag_id') + ->select('tag_id') + ->selectRaw('COUNT(*) as count') + ->orderBy('count', 'desc') + ->orderBy('tag_id', 'asc') + ->limit($limit); + } + public function beatmap() { return $this->belongsTo(Beatmap::class, 'beatmap_id'); diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 74293c5c00c..92134fa99ad 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -22,20 +22,4 @@ public function beatmapTags(): HasMany { return $this->hasMany(BeatmapTag::class); } - - public static function topTagIds(int $beatmapId, int $limit = 50) - { - return static - ::joinRelation( - 'beatmapTags', - fn ($q) => $q->where('beatmap_id', $beatmapId)->whereHas('user', fn ($userQuery) => $userQuery->default()) - ) - ->groupBy('id') - ->select('id') - ->selectRaw('COUNT(*) as count') - ->orderBy('count', 'desc') - ->orderBy('id', 'desc') - ->limit($limit) - ->get(); - } } diff --git a/app/Transformers/BeatmapsetCompactTransformer.php b/app/Transformers/BeatmapsetCompactTransformer.php index 59f47b44d71..f4f95e18cfd 100644 --- a/app/Transformers/BeatmapsetCompactTransformer.php +++ b/app/Transformers/BeatmapsetCompactTransformer.php @@ -303,7 +303,7 @@ public function includeRelatedUsers(Beatmapset $beatmapset) public function includeRelatedTags(Beatmapset $beatmapset) { $beatmaps = $this->beatmaps($beatmapset); - $tagIdSet = new Set($beatmaps->flatMap->topTagIds()->pluck('id')); + $tagIdSet = new Set($beatmaps->flatMap->topTagIds()->pluck('tag_id')); $cachedTags = app('tags'); $json = []; diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index e93119133f3..04ec53a20a5 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -96,7 +96,7 @@ export default class Controller { if (beatmap.top_tag_ids == null) continue; for (const tagId of beatmap.top_tag_ids) { - const tag = tagMap.get(tagId.id); + const tag = tagMap.get(tagId.tag_id); if (tag == null) continue; tag.count += tagId.count; diff --git a/resources/js/interfaces/beatmap-json.ts b/resources/js/interfaces/beatmap-json.ts index 9c5837a51ea..f00b9b24a14 100644 --- a/resources/js/interfaces/beatmap-json.ts +++ b/resources/js/interfaces/beatmap-json.ts @@ -17,7 +17,7 @@ interface BeatmapJsonAvailableIncludes { failtimes: BeatmapFailTimesArray; max_combo: number; owners: BeatmapOwnerJson[]; - top_tag_ids: { count: number; id: number }[]; + top_tag_ids: { count: number; tag_id: number }[]; user: UserJson; } From 715db0347cc4256d0a453dc23fe5d2d3bf983fc7 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 18 Dec 2024 15:41:00 +0900 Subject: [PATCH 13/78] copy and return typed value instead of messing with original object --- resources/js/beatmapsets-show/controller.ts | 15 ++++++++++++--- resources/js/interfaces/tag-json.ts | 3 --- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 04ec53a20a5..c4fc7f1fd53 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json'; -import { TagJsonWithCount } from 'interfaces/tag-json'; +import TagJson from 'interfaces/tag-json'; import UserJson from 'interfaces/user-json'; import { keyBy } from 'lodash'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; @@ -25,6 +25,16 @@ interface State { showingNsfwWarning: boolean; } +type TagJsonWithCount = TagJson & { count: number }; + +function asTagJsonWithCount(tag: TagJson) { + return { + count: 0, + ...tag, + }; +} + + export default class Controller { @observable hoveredBeatmap: null | BeatmapJsonForBeatmapsetShow = null; @observable state: State; @@ -88,8 +98,7 @@ export default class Controller { const tagMap = new Map(); for (const tag of this.beatmapset.related_tags) { - tag.count = 0; // assign 0 and cast - tagMap.set(tag.id, tag as TagJsonWithCount); + tagMap.set(tag.id, asTagJsonWithCount(tag)); } for (const beatmap of this.beatmapset.beatmaps) { diff --git a/resources/js/interfaces/tag-json.ts b/resources/js/interfaces/tag-json.ts index da857843edc..5d1352a7a4b 100644 --- a/resources/js/interfaces/tag-json.ts +++ b/resources/js/interfaces/tag-json.ts @@ -2,10 +2,7 @@ // See the LICENCE file in the repository root for full licence text. export default interface TagJson { - count?: number; description: string; id: number; name: string; } - -export type TagJsonWithCount = TagJson & Required>; From dd68826c02a7a4de654abc86457a89993d5dfb3b Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 18 Dec 2024 21:02:44 +0900 Subject: [PATCH 14/78] don't double fetch from redis for transformer --- app/Models/Beatmap.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/Models/Beatmap.php b/app/Models/Beatmap.php index 54205677699..3cca72263e3 100644 --- a/app/Models/Beatmap.php +++ b/app/Models/Beatmap.php @@ -8,6 +8,7 @@ use App\Exceptions\InvariantException; use App\Jobs\EsDocument; use App\Libraries\Transactions\AfterCommit; +use App\Traits\Memoizes; use DB; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; @@ -53,7 +54,7 @@ */ class Beatmap extends Model implements AfterCommit { - use SoftDeletes; + use Memoizes, SoftDeletes; public $convert = false; @@ -350,11 +351,15 @@ public function status() public function topTagIds() { - return cache_remember_mutexed( - "beatmap_top_tag_ids:{$this->getKey()}", - $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], - [], - fn () => BeatmapTag::topTagIdsQuery($this->getKey())->get()->toArray(), + // TODO: Add option to multi query when beatmapset requests all tags for beatmaps? + return $this->memoize( + __FUNCTION__, + fn () => cache_remember_mutexed( + "beatmap_top_tag_ids:{$this->getKey()}", + $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], + [], + fn () => BeatmapTag::topTagIdsQuery($this->getKey())->get()->toArray(), + ), ); } From 2b3bb426650d18485bd32964b82a19ef87cf1d8a Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 19 Dec 2024 16:32:55 +0900 Subject: [PATCH 15/78] should be unique --- database/factories/TagFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php index f1bb27b044d..8617d60d431 100644 --- a/database/factories/TagFactory.php +++ b/database/factories/TagFactory.php @@ -16,7 +16,7 @@ class TagFactory extends Factory public function definition(): array { return [ - 'name' => fn () => "Tag {$this->faker->word}", + 'name' => fn () => "Tag {$this->faker->unique()->word}", 'description' => fn () => $this->faker->sentence, ]; } From 05db2c1f21bf2d951f43ab5d2fe779e0efbf7be1 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 19 Dec 2024 19:49:11 +0900 Subject: [PATCH 16/78] removed/unused --- resources/js/beatmapsets-show/controller.ts | 10 ---------- resources/js/interfaces/beatmapset-json.ts | 1 - 2 files changed, 11 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index c4fc7f1fd53..3f2050db321 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -82,16 +82,6 @@ export default class Controller { return this.beatmaps.get(this.currentBeatmap.mode) ?? []; } - get relatedTags() { - const map = new Map(); - - for (const tag of this.beatmapset.related_tags) { - map.set(tag.id, tag); - } - - return map; - } - @computed get tags() { const mapperTagSet = new Set(this.beatmapset.tags.split(' ').filter(present)); diff --git a/resources/js/interfaces/beatmapset-json.ts b/resources/js/interfaces/beatmapset-json.ts index 6519a014d1d..f6203c3c052 100644 --- a/resources/js/interfaces/beatmapset-json.ts +++ b/resources/js/interfaces/beatmapset-json.ts @@ -96,7 +96,6 @@ interface BeatmapsetJsonAvailableIncludes { related_tags: TagJson[]; related_users: UserJson[]; user: UserJson | UserJsonDeleted; - user_tags: TagJson[]; } interface HypeData { From 0eefd0b23cd3da7217c286e2152130330f80a46c Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 19 Dec 2024 19:55:54 +0900 Subject: [PATCH 17/78] missing non-incrementing --- app/Models/BeatmapTag.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/BeatmapTag.php b/app/Models/BeatmapTag.php index 8b1cf46fd17..96a7be925ce 100644 --- a/app/Models/BeatmapTag.php +++ b/app/Models/BeatmapTag.php @@ -18,6 +18,7 @@ class BeatmapTag extends Model { protected $primaryKey = ':composite'; protected $primaryKeys = ['beatmap_id', 'tag_id', 'user_id']; + public $incrementing = false; public static function topTagIdsQuery(int $beatmapId, int $limit = 50) { From c9ad28dad76504b4a94430b4d03ead1fe48d0e6b Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 19 Dec 2024 20:01:41 +0900 Subject: [PATCH 18/78] memoize all --- app/Singletons/Tags.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/Singletons/Tags.php b/app/Singletons/Tags.php index b8cccbd6e46..589bad8b557 100644 --- a/app/Singletons/Tags.php +++ b/app/Singletons/Tags.php @@ -10,16 +10,25 @@ use App\Models\Tag; use App\Traits\Memoizes; use App\Transformers\TagTransformer; +use Illuminate\Support\Collection; class Tags { use Memoizes; + /** + * @return Collection + */ + public function all(): Collection + { + return $this->memoize(__FUNCTION__, fn () => Tag::all()); + } + public function get(int $id): ?Tag { $allById = $this->memoize( 'allById', - fn () => Tag::all()->keyBy('id'), + fn () => $this->all()->keyBy('id'), ); return $allById[$id] ?? null; @@ -29,7 +38,7 @@ public function json(): array { return $this->memoize( __FUNCTION__, - fn () => json_collection(Tag::all(), new TagTransformer()), + fn () => json_collection($this->all(), new TagTransformer()), ); } } From fc4b347e8716f26b63123254af06af3ab6ab6ac1 Mon Sep 17 00:00:00 2001 From: nanaya Date: Fri, 20 Dec 2024 16:51:55 +0900 Subject: [PATCH 19/78] Add disband team button --- app/Http/Controllers/TeamsController.php | 11 +++++++++++ app/Models/Team.php | 13 +++++++++++++ resources/lang/en/teams.php | 5 +++++ resources/views/teams/show.blade.php | 15 +++++++++++++++ routes/web.php | 2 +- 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/TeamsController.php b/app/Http/Controllers/TeamsController.php index 0792c39db0b..92b6de5f8f8 100644 --- a/app/Http/Controllers/TeamsController.php +++ b/app/Http/Controllers/TeamsController.php @@ -19,6 +19,17 @@ public function __construct() $this->middleware('auth', ['only' => ['part']]); } + public function destroy(string $id): Response + { + $team = Team::findOrFail($id); + priv_check('TeamUpdate', $team)->ensureCan(); + + $team->delete(); + \Session::flash('popup', osu_trans('teams.destroy.ok')); + + return ujs_redirect(route('home')); + } + public function edit(string $id): Response { $team = Team::findOrFail($id); diff --git a/app/Models/Team.php b/app/Models/Team.php index a4a324052a3..7db9acd9690 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -76,6 +76,19 @@ public function descriptionHtml(): string : bbcode((new BBCodeForDB($description))->generate()); } + public function delete() + { + $ret = parent::delete(); + + if ($ret) { + $this->header()->delete(); + $this->logo()->delete(); + $this->members()->delete(); + } + + return $ret; + } + public function header(): Uploader { return $this->header ??= new Uploader( diff --git a/resources/lang/en/teams.php b/resources/lang/en/teams.php index ae637b63d52..a3984e14dff 100644 --- a/resources/lang/en/teams.php +++ b/resources/lang/en/teams.php @@ -4,6 +4,10 @@ // See the LICENCE file in the repository root for full licence text. return [ + 'destroy' => [ + 'ok' => 'Team removed', + ], + 'edit' => [ 'saved' => 'Settings saved successfully', 'title' => 'Team Settings', @@ -66,6 +70,7 @@ 'show' => [ 'bar' => [ + 'destroy' => 'Disband Team', 'part' => 'Leave Team', ], diff --git a/resources/views/teams/show.blade.php b/resources/views/teams/show.blade.php index ed4c2d6e2ce..ae9e5927d8e 100644 --- a/resources/views/teams/show.blade.php +++ b/resources/views/teams/show.blade.php @@ -18,6 +18,9 @@ if (priv_check('TeamPart', $team)->can()) { $buttons->add('part'); } + if (priv_check('TeamUpdate', $team)->can()) { + $buttons->add('destroy'); + } @endphp @extends('master', [ @@ -76,6 +79,18 @@ class="btn-circle btn-circle--page-toggle"
@if (!$buttons->isEmpty())
+ @if ($buttons->contains('destroy')) +
+ + +
+ @endif @if ($buttons->contains('part'))
name('part'); Route::resource('members', 'Teams\MembersController', ['only' => ['destroy', 'index']]); }); - Route::resource('teams', 'TeamsController', ['only' => ['edit', 'show', 'update']]); + Route::resource('teams', 'TeamsController', ['only' => ['destroy', 'edit', 'show', 'update']]); Route::post('users/check-username-availability', 'UsersController@checkUsernameAvailability')->name('users.check-username-availability'); Route::get('users/lookup', 'Users\LookupController@index')->name('users.lookup'); From 421504c8350250e4c5fcb20dac98e86eff2299b5 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Fri, 20 Dec 2024 23:11:06 +0900 Subject: [PATCH 20/78] add composite key test; add missing timestamp properties --- app/Models/BeatmapTag.php | 2 ++ tests/Models/ModelCompositePrimaryKeysTest.php | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/app/Models/BeatmapTag.php b/app/Models/BeatmapTag.php index 96a7be925ce..fc7abf63ce8 100644 --- a/app/Models/BeatmapTag.php +++ b/app/Models/BeatmapTag.php @@ -10,7 +10,9 @@ /** * @property-read Beatmap $beatmap * @property int $beatmap_id + * @property \Carbon\Carbon $created_at * @property int $tag_id + * @property \Carbon\Carbon $updated_at * @property-read User $user * @property int $user_id */ diff --git a/tests/Models/ModelCompositePrimaryKeysTest.php b/tests/Models/ModelCompositePrimaryKeysTest.php index d2e02b3edac..58d8b97575f 100644 --- a/tests/Models/ModelCompositePrimaryKeysTest.php +++ b/tests/Models/ModelCompositePrimaryKeysTest.php @@ -10,6 +10,7 @@ use App\Models\BeatmapDifficulty; use App\Models\BeatmapDifficultyAttrib; use App\Models\BeatmapFailtimes; +use App\Models\BeatmapTag; use App\Models\Chat; use App\Models\FavouriteBeatmapset; use App\Models\Forum; @@ -112,6 +113,16 @@ public static function dataProviderBase() ['type' => 'exit'], ['p1', [0, 10], 11], ], + [ + BeatmapTag::class, + [ + 'beatmap_id' => 0, + 'tag_id' => 0, + 'user_id' => 0, + ], + ['tag_id' => 1], + ['updated_at', [Carbon::now()->subDays(5), Carbon::now()->subDays(1)], Carbon::now()], + ], [ Chat\UserChannel::class, [ From efad0be5359e5d2508d5cb1e1915a3889c03a45a Mon Sep 17 00:00:00 2001 From: bakaneko Date: Sat, 21 Dec 2024 00:20:47 +0900 Subject: [PATCH 21/78] tie break with names --- resources/js/beatmapsets-show/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 3f2050db321..df472a63140 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -112,7 +112,7 @@ export default class Controller { mapperTags: [...mapperTagSet.values()], userTags: [...tagMap.values()].sort((a, b) => { const diff = b.count - a.count; - return diff !== 0 ? diff : a.id - b.id; + return diff !== 0 ? diff : a.name.localeCompare(b.name); }), }; } From cde18d06af8beaee20e4c780c3f3b43a7e309454 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Sat, 21 Dec 2024 00:22:36 +0900 Subject: [PATCH 22/78] don't dedupe mapper tags since they might be names --- resources/js/beatmapsets-show/controller.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index df472a63140..15f9acc725d 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -84,7 +84,6 @@ export default class Controller { @computed get tags() { - const mapperTagSet = new Set(this.beatmapset.tags.split(' ').filter(present)); const tagMap = new Map(); for (const tag of this.beatmapset.related_tags) { @@ -99,17 +98,11 @@ export default class Controller { if (tag == null) continue; tag.count += tagId.count; - - // TODO: case insensitivity - if (mapperTagSet.has(tag.name)) { - tag.count++; - mapperTagSet.delete(tag.name); - } } } return { - mapperTags: [...mapperTagSet.values()], + mapperTags: this.beatmapset.tags.split(' ').filter(present), userTags: [...tagMap.values()].sort((a, b) => { const diff = b.count - a.count; return diff !== 0 ? diff : a.name.localeCompare(b.name); From 68c26056d2efa5ae988eca8259720e82e4d9aa07 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Dec 2024 20:11:26 +0900 Subject: [PATCH 23/78] Allow multiplayer scores with arbitrary beatmap/ruleset combinations --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index ea1f7321d1f..858305be77f 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -8,6 +8,7 @@ use App\Exceptions\InvariantException; use App\Http\Controllers\Controller as BaseController; use App\Libraries\ClientCheck; +use App\Models\Beatmap; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\PlaylistItemUserHighScore; use App\Models\Multiplayer\Room; @@ -184,8 +185,15 @@ public function store($roomId, $playlistId) $request = \Request::instance(); $params = $request->all(); - if (get_string($params['beatmap_hash'] ?? null) !== $playlistItem->beatmap->checksum) { - throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid')); + if ($playlistItem->beatmapset_id === null) { + if (get_string($params['beatmap_hash'] ?? null) !== $playlistItem->beatmap->checksum) { + throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid')); + } + } else { + // Todo: Validate beatmap_hash param matches any checksum from the playlist item's beatmap set. + // Todo: Modifying the playlist item looks dodgy to me and is likely failing some internal validations. + $playlistItem->beatmap_id = Beatmap::firstWhere('checksum', get_string($params['beatmap_hash'] ?? null))->beatmap_id; + $playlistItem->ruleset_id = get_int($params['ruleset_id'] ?? null); } $buildId = ClientCheck::parseToken($request)['buildId']; From 572b0461c9c5906a67def30741526111afb3c837 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Wed, 25 Dec 2024 08:02:38 +0900 Subject: [PATCH 24/78] show user tags per beatmap --- resources/js/beatmapsets-show/controller.ts | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index 15f9acc725d..a48de73ff41 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -83,27 +83,34 @@ export default class Controller { } @computed - get tags() { - const tagMap = new Map(); + get relatedTags() { + const map = new Map(); for (const tag of this.beatmapset.related_tags) { - tagMap.set(tag.id, asTagJsonWithCount(tag)); + map.set(tag.id, tag); } - for (const beatmap of this.beatmapset.beatmaps) { - if (beatmap.top_tag_ids == null) continue; + return map; + } + + @computed + get tags() { + const userTags: TagJsonWithCount[] = []; - for (const tagId of beatmap.top_tag_ids) { - const tag = tagMap.get(tagId.tag_id); - if (tag == null) continue; + if (this.currentBeatmap.top_tag_ids != null) { + for (const tagId of this.currentBeatmap.top_tag_ids) { + const maybeTag = this.relatedTags.get(tagId.tag_id); + if (maybeTag == null) continue; - tag.count += tagId.count; + const tag = asTagJsonWithCount(maybeTag); + tag.count = tagId.count; + userTags.push(asTagJsonWithCount(tag)); } } return { mapperTags: this.beatmapset.tags.split(' ').filter(present), - userTags: [...tagMap.values()].sort((a, b) => { + userTags: userTags.sort((a, b) => { const diff = b.count - a.count; return diff !== 0 ? diff : a.name.localeCompare(b.name); }), From f689c12610c0f8722307e812c2eaeb765c5e86ad Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 26 Dec 2024 08:07:45 +0900 Subject: [PATCH 25/78] exclude special groups from being set as beatmap owner (and being gifted supporter tags) --- app/Http/Controllers/Users/LookupController.php | 3 +-- app/Libraries/Beatmapset/ChangeBeatmapOwners.php | 2 +- app/Models/User.php | 9 +++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Users/LookupController.php b/app/Http/Controllers/Users/LookupController.php index afe871f58ef..725373624c2 100644 --- a/app/Http/Controllers/Users/LookupController.php +++ b/app/Http/Controllers/Users/LookupController.php @@ -35,8 +35,7 @@ public function index() } $users = User::where(fn ($q) => $q->whereIn('user_id', $numericIds)->orWhereIn('username', $stringIds)) - ->where('group_id', '<>', app('groups')->byIdentifier('no_profile')->getKey()) - ->default() + ->defaultForLookup() ->with(UserCompactTransformer::CARD_INCLUDES_PRELOAD) ->get(); diff --git a/app/Libraries/Beatmapset/ChangeBeatmapOwners.php b/app/Libraries/Beatmapset/ChangeBeatmapOwners.php index 87a869871ba..5b137b9a60d 100644 --- a/app/Libraries/Beatmapset/ChangeBeatmapOwners.php +++ b/app/Libraries/Beatmapset/ChangeBeatmapOwners.php @@ -43,7 +43,7 @@ public function handle(): void $newUserIds = $this->userIds->diff($currentOwners); - if (User::whereIn('user_id', $newUserIds->toArray())->default()->count() !== $newUserIds->count()) { + if (User::whereIn('user_id', $newUserIds->toArray())->defaultForLookup()->count() !== $newUserIds->count()) { throw new InvariantException('invalid user_id'); } diff --git a/app/Models/User.php b/app/Models/User.php index dcee75bbedc..7c951737fad 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2032,6 +2032,15 @@ public function scopeDefault($query) ]); } + public function scopeDefaultForLookup(Builder $query): Builder + { + $groups = app('groups'); + + return $query + ->whereNotIn('group_id', [$groups->byIdentifier('no_profile')->getKey(), $groups->byIdentifier('bot')->getKey()]) + ->default(); + } + public function scopeOnline($query) { return $query From dedc1b310201cbb47e0152683d93cb9aef7afb08 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 26 Dec 2024 18:49:24 +0900 Subject: [PATCH 26/78] add test --- .../Beatmapset/ChangeBeatmapOwnersTest.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/Libraries/Beatmapset/ChangeBeatmapOwnersTest.php b/tests/Libraries/Beatmapset/ChangeBeatmapOwnersTest.php index dcafe6da068..df6e815b4e4 100644 --- a/tests/Libraries/Beatmapset/ChangeBeatmapOwnersTest.php +++ b/tests/Libraries/Beatmapset/ChangeBeatmapOwnersTest.php @@ -23,6 +23,14 @@ class ChangeBeatmapOwnersTest extends TestCase { + public static function dataProviderForInvalidUser(): array + { + return [ + ['bot'], + ['no_profile'], + ]; + } + public static function dataProviderForUpdateOwner(): array { return [ @@ -68,6 +76,33 @@ public function testMissingUser(): void Bus::assertDispatched(BeatmapOwnerChange::class); } + /** + * @dataProvider dataProviderForInvalidUser + */ + public function testInvalidUser(string $group): void + { + $moderator = User::factory()->withGroup('nat')->create(); + $owner = User::factory()->create(); + $invalidUser = User::factory()->withGroup($group)->create(); + + $beatmap = Beatmap::factory() + ->for(Beatmapset::factory()->pending()->owner($owner)) + ->owner($owner) + ->create(); + + $this->expectCountChange(fn () => BeatmapsetEvent::count(), 0); + + $this->expectExceptionCallable( + fn () => (new ChangeBeatmapOwners($beatmap, [$invalidUser->getKey()], $moderator))->handle(), + InvariantException::class, + ); + + $beatmap = $beatmap->fresh(); + $this->assertEqualsCanonicalizing([$owner->getKey()], $beatmap->getOwners()->pluck('user_id')->toArray()); + + Bus::assertNotDispatched(BeatmapOwnerChange::class); + } + /** * @dataProvider dataProviderForUpdateOwner */ From 845dcfee38ed908f22f1a682f2e4d3c5c55819f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jan 2025 15:22:06 +0900 Subject: [PATCH 27/78] Add `osu_logins` migration For use in https://github.com/ppy/osu-server-spectator/pull/253. --- .../2025_01_06_000000_create_osu_logins.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 database/migrations/2025_01_06_000000_create_osu_logins.php diff --git a/database/migrations/2025_01_06_000000_create_osu_logins.php b/database/migrations/2025_01_06_000000_create_osu_logins.php new file mode 100644 index 00000000000..ff42c496bc7 --- /dev/null +++ b/database/migrations/2025_01_06_000000_create_osu_logins.php @@ -0,0 +1,36 @@ +. 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); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + public function up(): void + { + // This is a legacy table. Migration is added for external projects' sake. + if (Schema::hasTable('osu_logins')) { + return; + } + + Schema::create('osu_logins', function (Blueprint $table) { + $table->unsignedInteger('user_id')->default(0); + $table->string('ip', 100)->default(''); + $table->timestamp('date')->useCurrent(); + + $table->index('user_id'); + $table->index('date'); + $table->index('ip'); + }); + } + + public function down(): void + { + Schema::dropIfExists('teams'); + } +}; From 1cae1e27e78e649d122f97e354afe9dd0a9ebcb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Jan 2025 15:45:03 +0900 Subject: [PATCH 28/78] Add explicit index names and fix `down()` function Co-authored-by: Edho Arief --- .../migrations/2025_01_06_000000_create_osu_logins.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/migrations/2025_01_06_000000_create_osu_logins.php b/database/migrations/2025_01_06_000000_create_osu_logins.php index ff42c496bc7..6d0840afa43 100644 --- a/database/migrations/2025_01_06_000000_create_osu_logins.php +++ b/database/migrations/2025_01_06_000000_create_osu_logins.php @@ -23,14 +23,14 @@ public function up(): void $table->string('ip', 100)->default(''); $table->timestamp('date')->useCurrent(); - $table->index('user_id'); - $table->index('date'); - $table->index('ip'); + $table->index('user_id', 'user_id'); + $table->index('date', 'date'); + $table->index('ip', 'ip'); }); } public function down(): void { - Schema::dropIfExists('teams'); + Schema::dropIfExists('osu_logins'); } }; From 00f50652ed2d186b49ab03bce91a27fb6076eae6 Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Jan 2025 17:32:34 +0900 Subject: [PATCH 29/78] Delete files first and transact db things --- app/Models/Team.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/Models/Team.php b/app/Models/Team.php index 7db9acd9690..33437a2774f 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -78,15 +78,18 @@ public function descriptionHtml(): string public function delete() { - $ret = parent::delete(); + $this->header()->delete(); + $this->logo()->delete(); - if ($ret) { - $this->header()->delete(); - $this->logo()->delete(); - $this->members()->delete(); - } + return $this->getConnection()->transaction(function () { + $ret = parent::delete(); + + if ($ret) { + $this->members()->delete(); + } - return $ret; + return $ret; + }); } public function header(): Uploader From 450750ada3cca25d267b5c5197947f6fba509c2b Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Jan 2025 20:28:23 +0900 Subject: [PATCH 30/78] Add test --- tests/Models/TeamTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/Models/TeamTest.php diff --git a/tests/Models/TeamTest.php b/tests/Models/TeamTest.php new file mode 100644 index 00000000000..55293fe0619 --- /dev/null +++ b/tests/Models/TeamTest.php @@ -0,0 +1,29 @@ +. 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 Tests\Models; + +use App\Models\Team; +use App\Models\TeamMember; +use App\Models\User; +use Tests\TestCase; + +class TeamTest extends TestCase +{ + public function testDelete(): void + { + $team = Team::factory()->create(); + $team->members()->create(['user_id' => User::factory()->create()->getKey()]); + $otherTeam = Team::factory()->create(); + $otherTeam->members()->create(['user_id' => User::factory()->create()->getKey()]); + + $this->expectCountChange(fn () => Team::count(), -1); + $this->expectCountChange(fn () => TeamMember::count(), -2); + + $team->fresh()->delete(); + } +} From 3bc532202a7ae00fbb85bb15a1e86c887a2463ef Mon Sep 17 00:00:00 2001 From: nanaya Date: Mon, 6 Jan 2025 21:31:13 +0900 Subject: [PATCH 31/78] More test --- tests/Models/TeamTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Models/TeamTest.php b/tests/Models/TeamTest.php index 55293fe0619..f15d0ef6999 100644 --- a/tests/Models/TeamTest.php +++ b/tests/Models/TeamTest.php @@ -23,7 +23,10 @@ public function testDelete(): void $this->expectCountChange(fn () => Team::count(), -1); $this->expectCountChange(fn () => TeamMember::count(), -2); + $this->expectCountChange(fn () => $otherTeam->members()->count(), 0); $team->fresh()->delete(); + + $this->assertNotNull($otherTeam->fresh()); } } From 318d76f06c5b4d95d1d13fdefde9b1211569d629 Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 7 Jan 2025 15:37:27 +0900 Subject: [PATCH 32/78] Show user card on discussion page user link --- resources/js/beatmap-discussions/user-card.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/resources/js/beatmap-discussions/user-card.tsx b/resources/js/beatmap-discussions/user-card.tsx index 53ab7e3e283..71a34658c74 100644 --- a/resources/js/beatmap-discussions/user-card.tsx +++ b/resources/js/beatmap-discussions/user-card.tsx @@ -3,6 +3,7 @@ import UserAvatar from 'components/user-avatar'; import UserGroupBadge from 'components/user-group-badge'; +import UserLink from 'components/user-link'; import UserGroupJson from 'interfaces/user-group-json'; import UserJson from 'interfaces/user-json'; import { route } from 'laroute'; @@ -32,12 +33,12 @@ export class UserCard extends React.PureComponent { ) : ( - - + )}
@@ -49,12 +50,12 @@ export class UserCard extends React.PureComponent { ) : ( - {this.renderUsername()} - + )} {!this.props.user.is_bot && !this.props.user.is_deleted && ( Date: Tue, 7 Jan 2025 20:10:36 +0900 Subject: [PATCH 33/78] Force browser page refresh on locale change --- app/Http/Controllers/HomeController.php | 2 +- resources/views/layout/ujs_full_reload.blade.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 resources/views/layout/ujs_full_reload.blade.php diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 0cd6ff043d9..b1a1b491fd0 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -210,7 +210,7 @@ public function setLocale() ]); } - return ext_view('layout.ujs-reload', [], 'js') + return ext_view('layout.ujs_full_reload', [], 'js') ->withCookie(cookie()->forever('locale', $newLocale)); } diff --git a/resources/views/layout/ujs_full_reload.blade.php b/resources/views/layout/ujs_full_reload.blade.php new file mode 100644 index 00000000000..58a93c38b26 --- /dev/null +++ b/resources/views/layout/ujs_full_reload.blade.php @@ -0,0 +1,5 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} +window.location.reload(); From 8a72a2a4c7679f6c7a8d608d9990b8902b679f87 Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 7 Jan 2025 20:19:21 +0900 Subject: [PATCH 34/78] Reset changelog chart before creating a new one --- .../js/core-legacy/changelog-chart-loader.coffee | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/js/core-legacy/changelog-chart-loader.coffee b/resources/js/core-legacy/changelog-chart-loader.coffee index 2e6a570da3f..75a0e885da7 100644 --- a/resources/js/core-legacy/changelog-chart-loader.coffee +++ b/resources/js/core-legacy/changelog-chart-loader.coffee @@ -10,11 +10,18 @@ export default class ChangelogChartLoader $(window).on 'resize', @resize $(document).on 'turbo:load', @initialize + initialize: => - return if !@container[0]? + container = @container[0] + + return unless container? + + # reset existing chart + container.innerHTML = '' + + container._chart = new ChangelogChart container + container._chart.loadData() - @container[0]._chart = new ChangelogChart @container[0] - @container[0]._chart.loadData() resize: => @container[0]?._chart.resize() From f713a0e736dd0d27abfc76dfd8586371d594315a Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 7 Jan 2025 20:22:20 +0900 Subject: [PATCH 35/78] Simplify existence check --- .../js/core-legacy/changelog-chart-loader.coffee | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/resources/js/core-legacy/changelog-chart-loader.coffee b/resources/js/core-legacy/changelog-chart-loader.coffee index 75a0e885da7..884ca05d09d 100644 --- a/resources/js/core-legacy/changelog-chart-loader.coffee +++ b/resources/js/core-legacy/changelog-chart-loader.coffee @@ -4,24 +4,22 @@ import ChangelogChart from 'charts/changelog-chart' export default class ChangelogChartLoader - container: document.getElementsByClassName('js-changelog-chart') - constructor: -> $(window).on 'resize', @resize $(document).on 'turbo:load', @initialize initialize: => - container = @container[0] + @container = document.querySelector('.js-changelog-chart') - return unless container? + return unless @container? # reset existing chart - container.innerHTML = '' + @container.innerHTML = '' - container._chart = new ChangelogChart container - container._chart.loadData() + @container._chart = new ChangelogChart @container + @container._chart.loadData() resize: => - @container[0]?._chart.resize() + @container?._chart.resize() From 8347eb5a18491b37c5a5da69d8496b369044d2b5 Mon Sep 17 00:00:00 2001 From: nanaya Date: Tue, 7 Jan 2025 20:27:58 +0900 Subject: [PATCH 36/78] More precise chart event handling --- .../core-legacy/changelog-chart-loader.coffee | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/resources/js/core-legacy/changelog-chart-loader.coffee b/resources/js/core-legacy/changelog-chart-loader.coffee index 884ca05d09d..25c056497aa 100644 --- a/resources/js/core-legacy/changelog-chart-loader.coffee +++ b/resources/js/core-legacy/changelog-chart-loader.coffee @@ -5,21 +5,27 @@ import ChangelogChart from 'charts/changelog-chart' export default class ChangelogChartLoader constructor: -> - $(window).on 'resize', @resize - $(document).on 'turbo:load', @initialize + document.addEventListener 'turbo:load', @initialize + document.addEventListener 'turbo:before-cache', @reset initialize: => - @container = document.querySelector('.js-changelog-chart') + container = document.querySelector('.js-changelog-chart') - return unless @container? + return unless container? # reset existing chart - @container.innerHTML = '' + container.innerHTML = '' - @container._chart = new ChangelogChart @container - @container._chart.loadData() + @chart = new ChangelogChart container + @chart.loadData() + window.addEventListener 'resize', @resize + + + reset: => + @chart = null + window.removeEventListener 'resize', @resize resize: => - @container?._chart.resize() + @chart.resize() From fa578457f300a9cddfbadd5e85131542ebd0b3e0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 15:37:27 +0900 Subject: [PATCH 37/78] Modify to boolean 'freestyle' parameter instead --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 13 +++++++------ app/Models/Multiplayer/PlaylistItem.php | 4 ++-- .../Multiplayer/PlaylistItemTransformer.php | 2 +- ...add_freestyle_to_multiplayer_playlist_items.php} | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) rename database/migrations/{2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php => 2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php} (79%) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 858305be77f..daf52f82020 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -185,15 +185,16 @@ public function store($roomId, $playlistId) $request = \Request::instance(); $params = $request->all(); - if ($playlistItem->beatmapset_id === null) { + if ($playlistItem->freestyle) { + // Todo: Ensure beatmap_hash matches any beatmap from the playlist item's beatmap set. + // Todo: Ensure ruleset_id is valid (converts only allowed for id=0 otherwise must match beatmap playmode, and value within 0..3). + // Todo: Modifying the playlist item looks dodgy to me :)! + $playlistItem->beatmap_id = Beatmap::firstWhere('checksum', get_string($params['beatmap_hash'] ?? null))->beatmap_id; + $playlistItem->ruleset_id = get_int($params['ruleset_id'] ?? null); + } else { if (get_string($params['beatmap_hash'] ?? null) !== $playlistItem->beatmap->checksum) { throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid')); } - } else { - // Todo: Validate beatmap_hash param matches any checksum from the playlist item's beatmap set. - // Todo: Modifying the playlist item looks dodgy to me and is likely failing some internal validations. - $playlistItem->beatmap_id = Beatmap::firstWhere('checksum', get_string($params['beatmap_hash'] ?? null))->beatmap_id; - $playlistItem->ruleset_id = get_int($params['ruleset_id'] ?? null); } $buildId = ClientCheck::parseToken($request)['buildId']; diff --git a/app/Models/Multiplayer/PlaylistItem.php b/app/Models/Multiplayer/PlaylistItem.php index 73d70ae77ba..b8ab7d8b56b 100644 --- a/app/Models/Multiplayer/PlaylistItem.php +++ b/app/Models/Multiplayer/PlaylistItem.php @@ -16,12 +16,12 @@ * @property json|null $allowed_mods * @property Beatmap $beatmap * @property int $beatmap_id - * @property int|null $beatmapset_id * @property \Carbon\Carbon|null $created_at * @property int $id * @property int $owner_id * @property int|null $playlist_order * @property json|null $required_mods + * @property bool $freestyle * @property Room $room * @property int $room_id * @property int|null $ruleset_id @@ -65,7 +65,7 @@ public static function fromJsonParams(User $owner, $json) $obj->$field = $value; } - $obj->beatmapset_id = get_int($json['beatmapset_id'] ?? null); + $obj->freestyle = get_bool($json['freestyle'] ?? false); $obj->max_attempts = get_int($json['max_attempts'] ?? null); $modsHelper = app('mods'); diff --git a/app/Transformers/Multiplayer/PlaylistItemTransformer.php b/app/Transformers/Multiplayer/PlaylistItemTransformer.php index 7caf2ab77ab..f7b331b05e9 100644 --- a/app/Transformers/Multiplayer/PlaylistItemTransformer.php +++ b/app/Transformers/Multiplayer/PlaylistItemTransformer.php @@ -21,10 +21,10 @@ public function transform(PlaylistItem $item) 'id' => $item->id, 'room_id' => $item->room_id, 'beatmap_id' => $item->beatmap_id, - 'beatmapset_id' => $item->beatmapset_id, 'ruleset_id' => $item->ruleset_id, 'allowed_mods' => $item->allowed_mods, 'required_mods' => $item->required_mods, + 'freestyle' => $item->freestyle, 'expired' => $item->expired, 'owner_id' => $item->owner_id, 'playlist_order' => $item->playlist_order, diff --git a/database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php similarity index 79% rename from database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php rename to database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php index 70106407a55..d8c3d6bd5c9 100644 --- a/database/migrations/2024_12_10_035240_add_beatmapset_id_to_multiplayer_playlist_items.php +++ b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php @@ -12,7 +12,7 @@ public function up(): void { Schema::table('multiplayer_playlist_items', function (Blueprint $table) { - $table->unsignedMediumInteger('beatmapset_id')->nullable()->after('beatmap_id'); + $table->boolean('freestyle')->after('required_mods')->default(false); }); } @@ -23,7 +23,7 @@ public function up(): void public function down(): void { Schema::table('multiplayer_playlist_items', function (Blueprint $table) { - $table->dropColumn('beatmapset_id'); + $table->dropColumn('freestyle'); }); } }; From 4699f50af05ab3908e9f33022eee0a6dc9e85ed9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 19:01:00 +0900 Subject: [PATCH 38/78] Require strict types + add license header --- ...10_035240_add_freestyle_to_multiplayer_playlist_items.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php index d8c3d6bd5c9..50b6076cbab 100644 --- a/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php +++ b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php @@ -1,5 +1,10 @@ . 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); + use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; From 2ecb1f04665aa95737c66895e943a41471384727 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 8 Jan 2025 19:14:12 +0900 Subject: [PATCH 39/78] Fix inspection --- ..._12_10_035240_add_freestyle_to_multiplayer_playlist_items.php | 1 - 1 file changed, 1 deletion(-) diff --git a/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php index 50b6076cbab..9136afe63b2 100644 --- a/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php +++ b/database/migrations/2024_12_10_035240_add_freestyle_to_multiplayer_playlist_items.php @@ -19,7 +19,6 @@ public function up(): void Schema::table('multiplayer_playlist_items', function (Blueprint $table) { $table->boolean('freestyle')->after('required_mods')->default(false); }); - } /** From cf50e17f6902c2c424b4cc8861e7b8f2848827e8 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 8 Jan 2025 21:15:00 +0900 Subject: [PATCH 40/78] Rework score token validation Mainly in relation to multiplayer play. --- .../Rooms/Playlist/ScoresController.php | 8 +---- .../Controllers/ScoreTokensController.php | 35 +++++++------------ app/Models/Multiplayer/Room.php | 5 +-- app/Models/ScoreToken.php | 33 +++++++++++++++++ database/factories/ScoreTokenFactory.php | 3 +- .../Rooms/Playlist/ScoresControllerTest.php | 2 +- .../Controllers/ScoreTokensControllerTest.php | 22 ++++-------- tests/Models/Multiplayer/RoomTest.php | 16 ++++----- .../Multiplayer/UserScoreAggregateTest.php | 2 +- tests/TestCase.php | 13 ++++++- 10 files changed, 80 insertions(+), 59 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index ea1f7321d1f..58ea39c74df 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers\Multiplayer\Rooms\Playlist; -use App\Exceptions\InvariantException; use App\Http\Controllers\Controller as BaseController; use App\Libraries\ClientCheck; use App\Models\Multiplayer\PlaylistItem; @@ -182,15 +181,10 @@ public function store($roomId, $playlistId) $playlistItem = $room->playlist()->findOrFail($playlistId); $user = \Auth::user(); $request = \Request::instance(); - $params = $request->all(); - - if (get_string($params['beatmap_hash'] ?? null) !== $playlistItem->beatmap->checksum) { - throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid')); - } $buildId = ClientCheck::parseToken($request)['buildId']; - $scoreToken = $room->startPlay($user, $playlistItem, $buildId); + $scoreToken = $room->startPlay($user, $playlistItem, $buildId, $request->all()); return json_item($scoreToken, new ScoreTokenTransformer()); } diff --git a/app/Http/Controllers/ScoreTokensController.php b/app/Http/Controllers/ScoreTokensController.php index 06a392e7358..b135d246d3d 100644 --- a/app/Http/Controllers/ScoreTokensController.php +++ b/app/Http/Controllers/ScoreTokensController.php @@ -29,33 +29,22 @@ public function store($beatmapId) $beatmap = Beatmap::increasesStatistics()->findOrFail($beatmapId); $user = auth()->user(); $request = \Request::instance(); - $params = get_params($request->all(), null, [ - 'beatmap_hash', - 'ruleset_id:int', - ]); - - $checks = [ - 'beatmap_hash' => fn (string $value): bool => $value === $beatmap->checksum, - 'ruleset_id' => fn (int $value): bool => Beatmap::modeStr($value) !== null && $beatmap->canBeConvertedTo($value), - ]; - foreach ($checks as $key => $testFn) { - if (!isset($params[$key])) { - throw new InvariantException("missing {$key}"); - } - if (!$testFn($params[$key])) { - throw new InvariantException("invalid {$key}"); - } - } $buildId = ClientCheck::parseToken($request)['buildId']; + $scoreToken = new ScoreToken([ + 'beatmap_id' => $beatmap->getKey(), + 'build_id' => $buildId, + 'user_id' => $user->getKey(), + ...get_params($request->all(), null, [ + 'beatmap_hash', + 'ruleset_id:int', + ]), + ]); + $scoreToken->setRelation('beatmap', $beatmap); + try { - $scoreToken = ScoreToken::create([ - 'beatmap_id' => $beatmap->getKey(), - 'build_id' => $buildId, - 'ruleset_id' => $params['ruleset_id'], - 'user_id' => $user->getKey(), - ]); + $scoreToken->saveOrExplode(); } catch (PDOException $e) { // TODO: move this to be a validation inside Score model throw new InvariantException('failed creating score token'); diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index c50ed5d63e3..2f2abf7e3c1 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -658,13 +658,13 @@ public function endGame(User $requestingUser) $this->save(); } - public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId) + public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId, array $rawParams): ScoreToken { priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan(); $this->assertValidStartPlay($user, $playlistItem); - return $this->getConnection()->transaction(function () use ($buildId, $user, $playlistItem) { + return $this->getConnection()->transaction(function () use ($buildId, $playlistItem, $rawParams, $user) { $agg = UserScoreAggregate::new($user, $this); if ($agg->wasRecentlyCreated) { $this->incrementInstance('participant_count'); @@ -676,6 +676,7 @@ public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId) $playlistItemAgg->updateUserAttempts(); return ScoreToken::create([ + 'beatmap_hash' => get_string($rawParams['beatmap_hash'] ?? null), 'beatmap_id' => $playlistItem->beatmap_id, 'build_id' => $buildId, 'playlist_item_id' => $playlistItem->getKey(), diff --git a/app/Models/ScoreToken.php b/app/Models/ScoreToken.php index f75c4bb07c5..5427c55b505 100644 --- a/app/Models/ScoreToken.php +++ b/app/Models/ScoreToken.php @@ -5,6 +5,7 @@ namespace App\Models; +use App\Exceptions\InvariantException; use App\Models\Multiplayer\PlaylistItem; use App\Models\Solo\Score; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -25,6 +26,8 @@ */ class ScoreToken extends Model { + public ?string $beatmapHash = null; + public function beatmap() { return $this->belongsTo(Beatmap::class, 'beatmap_id'); @@ -74,4 +77,34 @@ public function getAttribute($key) 'user' => $this->getRelationValue($key), }; } + + public function setBeatmapHashAttribute(?string $value): void + { + $this->beatmapHash = $value; + } + + public function assertValid(): void + { + $beatmap = $this->beatmap; + if ($this->beatmapHash !== $beatmap->checksum) { + throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid')); + } + + $rulesetId = $this->ruleset_id; + if ($rulesetId === null) { + throw new InvariantException('missing ruleset_id'); + } + if (Beatmap::modeStr($rulesetId) === null || !$beatmap->canBeConvertedTo($rulesetId)) { + throw new InvariantException('invalid ruleset_id'); + } + } + + public function save(array $options = []): bool + { + if (!$this->exists) { + $this->assertValid(); + } + + return parent::save($options); + } } diff --git a/database/factories/ScoreTokenFactory.php b/database/factories/ScoreTokenFactory.php index aa7f4950d14..7c7b85f727a 100644 --- a/database/factories/ScoreTokenFactory.php +++ b/database/factories/ScoreTokenFactory.php @@ -23,7 +23,8 @@ public function definition(): array 'build_id' => Build::factory(), 'user_id' => User::factory(), - // depends on beatmap_id + // depend on beatmap_id + 'beatmap_hash' => fn (array $attr) => Beatmap::find($attr['beatmap_id'])->checksum, 'ruleset_id' => fn (array $attr) => Beatmap::find($attr['beatmap_id'])->playmode, ]; } diff --git a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php index 2477e8d1518..bdbe0d69f72 100644 --- a/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php +++ b/tests/Controllers/Multiplayer/Rooms/Playlist/ScoresControllerTest.php @@ -169,7 +169,7 @@ public function testUpdate($bodyParams, $status) $playlistItem = PlaylistItem::factory()->create(); $room = $playlistItem->room; $build = Build::factory()->create(['allow_ranking' => true]); - $scoreToken = $room->startPlay($user, $playlistItem, 0); + $scoreToken = static::roomStartPlay($user, $playlistItem); $this->withHeaders(['x-token' => static::createClientToken($build)]); diff --git a/tests/Controllers/ScoreTokensControllerTest.php b/tests/Controllers/ScoreTokensControllerTest.php index e75d971fee0..1dc2c84a305 100644 --- a/tests/Controllers/ScoreTokensControllerTest.php +++ b/tests/Controllers/ScoreTokensControllerTest.php @@ -45,7 +45,7 @@ public function testStore(string $beatmapState, int $status): void /** * @dataProvider dataProviderForTestStoreInvalidParameter */ - public function testStoreInvalidParameter(string $paramKey, ?string $paramValue, int $status): void + public function testStoreInvalidParameter(string $paramKey, ?string $paramValue, int $status, string $errorMessage): void { $origClientCheckVersion = $GLOBALS['cfg']['osu']['client']['check_version']; config_set('osu.client.check_version', true); @@ -78,14 +78,6 @@ public function testStoreInvalidParameter(string $paramKey, ?string $paramValue, $this->expectCountChange(fn () => ScoreToken::count(), 0); - $errorMessage = $paramValue === null ? 'missing' : 'invalid'; - $errorMessage .= ' '; - $errorMessage .= $paramKey === 'client_token' - ? ($paramValue === null - ? 'token header' - : 'client hash' - ) : $paramKey; - $this->json( 'POST', route('api.beatmaps.solo.score-tokens.store', $routeParams), @@ -161,14 +153,14 @@ public static function dataProviderForTestStore(): array public static function dataProviderForTestStoreInvalidParameter(): array { return [ - 'invalid client token' => ['client_token', md5('invalid_'), 422], - 'missing client token' => ['client_token', null, 422], + 'invalid client token' => ['client_token', md5('invalid_'), 422, 'invalid client hash'], + 'missing client token' => ['client_token', null, 422, 'missing token header'], - 'invalid ruleset id' => ['ruleset_id', '5', 422], - 'missing ruleset id' => ['ruleset_id', null, 422], + 'invalid ruleset id' => ['ruleset_id', '5', 422, 'invalid ruleset_id'], + 'missing ruleset id' => ['ruleset_id', null, 422, 'missing ruleset_id'], - 'invalid beatmap hash' => ['beatmap_hash', 'xxx', 422], - 'missing beatmap hash' => ['beatmap_hash', null, 422], + 'invalid beatmap hash' => ['beatmap_hash', 'xxx', 422, 'invalid or missing beatmap_hash'], + 'missing beatmap hash' => ['beatmap_hash', null, 422, 'invalid or missing beatmap_hash'], ]; } diff --git a/tests/Models/Multiplayer/RoomTest.php b/tests/Models/Multiplayer/RoomTest.php index 3a1f8307417..ca8a951478b 100644 --- a/tests/Models/Multiplayer/RoomTest.php +++ b/tests/Models/Multiplayer/RoomTest.php @@ -124,7 +124,7 @@ public function testRoomHasEnded() ]); $this->expectException(InvariantException::class); - $room->startPlay($user, $playlistItem, 0); + static::roomStartPlay($user, $playlistItem); } public function testStartPlay(): void @@ -137,7 +137,7 @@ public function testStartPlay(): void $this->expectCountChange(fn () => $room->userHighScores()->count(), 1); $this->expectCountChange(fn () => $playlistItem->scoreTokens()->count(), 1); - $room->startPlay($user, $playlistItem, 0); + static::roomStartPlay($user, $playlistItem); $room->refresh(); $this->assertSame($user->getKey(), $playlistItem->scoreTokens()->last()->user_id); @@ -150,14 +150,14 @@ public function testMaxAttemptsReached() $playlistItem1 = PlaylistItem::factory()->create(['room_id' => $room]); $playlistItem2 = PlaylistItem::factory()->create(['room_id' => $room]); - $room->startPlay($user, $playlistItem1, 0); + static::roomStartPlay($user, $playlistItem1); $this->assertTrue(true); - $room->startPlay($user, $playlistItem2, 0); + static::roomStartPlay($user, $playlistItem2); $this->assertTrue(true); $this->expectException(InvariantException::class); - $room->startPlay($user, $playlistItem1, 0); + static::roomStartPlay($user, $playlistItem1); } public function testMaxAttemptsForItemReached() @@ -174,19 +174,19 @@ public function testMaxAttemptsForItemReached() ]); $initialCount = $playlistItem1->scoreTokens()->count(); - $room->startPlay($user, $playlistItem1, 0); + static::roomStartPlay($user, $playlistItem1); $this->assertSame($initialCount + 1, $playlistItem1->scoreTokens()->count()); $initialCount = $playlistItem1->scoreTokens()->count(); try { - $room->startPlay($user, $playlistItem1, 0); + static::roomStartPlay($user, $playlistItem1); } catch (Exception $ex) { $this->assertTrue($ex instanceof InvariantException); } $this->assertSame($initialCount, $playlistItem1->scoreTokens()->count()); $initialCount = $playlistItem2->scoreTokens()->count(); - $room->startPlay($user, $playlistItem2, 0); + static::roomStartPlay($user, $playlistItem2); $this->assertSame($initialCount + 1, $playlistItem2->scoreTokens()->count()); } diff --git a/tests/Models/Multiplayer/UserScoreAggregateTest.php b/tests/Models/Multiplayer/UserScoreAggregateTest.php index 710e5d52399..8ab32f33a75 100644 --- a/tests/Models/Multiplayer/UserScoreAggregateTest.php +++ b/tests/Models/Multiplayer/UserScoreAggregateTest.php @@ -173,7 +173,7 @@ public function testStartingPlayIncreasesAttempts(): void $user = User::factory()->create(); $playlistItem = $this->createPlaylistItem(); - $this->room->startPlay($user, $playlistItem, 0); + static::roomStartPlay($user, $playlistItem); $agg = UserScoreAggregate::new($user, $this->room); $this->assertSame(1, $agg->attempts); diff --git a/tests/TestCase.php b/tests/TestCase.php index 03c9ada27fe..1b4af0dae93 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,7 @@ use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\ScoreLink; use App\Models\OAuth\Client; +use App\Models\ScoreToken; use App\Models\User; use Artisan; use Carbon\CarbonInterface; @@ -111,7 +112,7 @@ protected static function resetAppDb(DatabaseManager $database): void protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, array $scoreParams): ScoreLink { return $playlistItem->room->completePlay( - $playlistItem->room->startPlay($user, $playlistItem, 0), + static::roomStartPlay($user, $playlistItem), [ 'accuracy' => 0.5, 'beatmap_id' => $playlistItem->beatmap_id, @@ -126,6 +127,16 @@ protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, ar ); } + protected static function roomStartPlay(User $user, PlaylistItem $playlistItem): ScoreToken + { + return $playlistItem->room->startPlay( + $user, + $playlistItem, + 0, + ['beatmap_hash' => $playlistItem->beatmap->checksum] + ); + } + protected function setUp(): void { $this->beforeApplicationDestroyed(fn () => $this->runExpectedCountsCallbacks()); From cdf9c223d6dcedc6ee07cf43bbb9fecfa8c599a5 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 8 Jan 2025 22:15:58 +0900 Subject: [PATCH 41/78] Fix tag controller tests --- tests/TestCase.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 03c9ada27fe..3617fbd4993 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -135,6 +135,11 @@ protected function setUp(): void // change config setting because we need more than 1 for the tests. config_set('osu.oauth.max_user_clients', 100); + // Disable caching for the BeatmapTagsController and TagsController tests + // because otherwise multiple run of the tests may use stale cache data. + config_set('osu.tags.beatmap_tags_cache_duration', 0); + config_set('osu.tags.tags_cache_duration', 0); + // Force connections to reset even if transactional tests were not used. // Should fix tests going wonky when different queue drivers are used, or anything that // breaks assumptions of object destructor timing. From a148f9f5f4d9e4f71b5f7148c2dcae04b65270c3 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 8 Jan 2025 22:32:46 +0900 Subject: [PATCH 42/78] Inline passing build id --- .../Multiplayer/Rooms/Playlist/ScoresController.php | 7 ++++--- app/Http/Controllers/ScoreTokensController.php | 4 +--- app/Models/Multiplayer/Room.php | 6 +++--- tests/TestCase.php | 10 ++++------ 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 58ea39c74df..19b79dde254 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -182,9 +182,10 @@ public function store($roomId, $playlistId) $user = \Auth::user(); $request = \Request::instance(); - $buildId = ClientCheck::parseToken($request)['buildId']; - - $scoreToken = $room->startPlay($user, $playlistItem, $buildId, $request->all()); + $scoreToken = $room->startPlay($user, $playlistItem, [ + ...$request->all(), + 'build_id' => ClientCheck::parseToken($request)['buildId'], + ]); return json_item($scoreToken, new ScoreTokenTransformer()); } diff --git a/app/Http/Controllers/ScoreTokensController.php b/app/Http/Controllers/ScoreTokensController.php index b135d246d3d..7f352f081aa 100644 --- a/app/Http/Controllers/ScoreTokensController.php +++ b/app/Http/Controllers/ScoreTokensController.php @@ -30,11 +30,9 @@ public function store($beatmapId) $user = auth()->user(); $request = \Request::instance(); - $buildId = ClientCheck::parseToken($request)['buildId']; - $scoreToken = new ScoreToken([ 'beatmap_id' => $beatmap->getKey(), - 'build_id' => $buildId, + 'build_id' => ClientCheck::parseToken($request)['buildId'], 'user_id' => $user->getKey(), ...get_params($request->all(), null, [ 'beatmap_hash', diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 2f2abf7e3c1..30041b14c4b 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -658,13 +658,13 @@ public function endGame(User $requestingUser) $this->save(); } - public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId, array $rawParams): ScoreToken + public function startPlay(User $user, PlaylistItem $playlistItem, array $rawParams): ScoreToken { priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan(); $this->assertValidStartPlay($user, $playlistItem); - return $this->getConnection()->transaction(function () use ($buildId, $playlistItem, $rawParams, $user) { + return $this->getConnection()->transaction(function () use ($playlistItem, $rawParams, $user) { $agg = UserScoreAggregate::new($user, $this); if ($agg->wasRecentlyCreated) { $this->incrementInstance('participant_count'); @@ -678,7 +678,7 @@ public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId, return ScoreToken::create([ 'beatmap_hash' => get_string($rawParams['beatmap_hash'] ?? null), 'beatmap_id' => $playlistItem->beatmap_id, - 'build_id' => $buildId, + 'build_id' => $rawParams['build_id'], 'playlist_item_id' => $playlistItem->getKey(), 'ruleset_id' => $playlistItem->ruleset_id, 'user_id' => $user->getKey(), diff --git a/tests/TestCase.php b/tests/TestCase.php index 1b4af0dae93..a2ef32784db 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -129,12 +129,10 @@ protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, ar protected static function roomStartPlay(User $user, PlaylistItem $playlistItem): ScoreToken { - return $playlistItem->room->startPlay( - $user, - $playlistItem, - 0, - ['beatmap_hash' => $playlistItem->beatmap->checksum] - ); + return $playlistItem->room->startPlay($user, $playlistItem, [ + 'beatmap_hash' => $playlistItem->beatmap->checksum, + 'build_id' => 0, + ]); } protected function setUp(): void From 9fc4d20ebd5722c395b08e6d9ba07461bffd9803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 10:22:40 +0100 Subject: [PATCH 43/78] Add completion marker to daily challenge profile counter Implements https://github.com/ppy/osu-web/issues/11597 for the website. --- resources/css/bem/daily-challenge.less | 26 ++++++++++++-- resources/js/profile-page/daily-challenge.tsx | 35 +++++++++++-------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/resources/css/bem/daily-challenge.less b/resources/css/bem/daily-challenge.less index d0353dd25c1..0d7d2aa6ddf 100644 --- a/resources/css/bem/daily-challenge.less +++ b/resources/css/bem/daily-challenge.less @@ -5,9 +5,29 @@ background: hsl(var(--hsl-b4)); border-radius: @border-radius-large; min-width: 0; - display: flex; - align-items: center; - padding: 3px; + position: relative; + + &--played-today { + border: 2px solid @osu-colour-lime-1; + + &::before { + .fas(); + background-color: @osu-colour-b6; + border-radius: 50%; + color: @osu-colour-lime-1; + content: @fa-var-check-circle; + font-size: 16px; + position: absolute; + right: -8px; + top: -8px; + } + } + + &__content { + display: flex; + align-items: center; + padding: 3px; + } &__name { font-size: @font-size--normal; diff --git a/resources/js/profile-page/daily-challenge.tsx b/resources/js/profile-page/daily-challenge.tsx index 44cf465e2c4..79d5a511704 100644 --- a/resources/js/profile-page/daily-challenge.tsx +++ b/resources/js/profile-page/daily-challenge.tsx @@ -4,6 +4,7 @@ import DailyChallengeUserStatsJson from 'interfaces/daily-challenge-user-stats-json'; import { autorun } from 'mobx'; import { observer } from 'mobx-react'; +import * as moment from 'moment'; import * as React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { classWithModifiers, Modifiers } from 'utils/css'; @@ -122,26 +123,30 @@ export default class DailyChallenge extends React.Component { return null; } + const playedToday = this.props.stats.last_update != null && moment.utc(this.props.stats.last_update).isSame(Date.now(), 'day'); + return (
-
- {trans('users.show.daily_challenge.title').split('\\n').map((line, i) => ( -
{line}
- ))} -
-
-
- {trans( - 'users.show.daily_challenge.unit.day', - { value: formatNumber(this.props.stats.playcount) }, - )} +
+
+ {trans('users.show.daily_challenge.title').split('\\n').map((line, i) => ( +
{line}
+ ))} +
+
+
+ {trans( + 'users.show.daily_challenge.unit.day', + { value: formatNumber(this.props.stats.playcount) }, + )} +
From b8adbac16b6ba1c94a5f114d2a675959076405c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 10:26:06 +0100 Subject: [PATCH 44/78] Adjust radius of inner box As mentioned in https://github.com/ppy/osu-web/issues/11597. --- resources/css/bem/daily-challenge.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/css/bem/daily-challenge.less b/resources/css/bem/daily-challenge.less index 0d7d2aa6ddf..e3e97b527c4 100644 --- a/resources/css/bem/daily-challenge.less +++ b/resources/css/bem/daily-challenge.less @@ -40,7 +40,7 @@ } &__value-box { - border-radius: @border-radius-large; + border-radius: @border-radius-small; background: hsl(var(--hsl-b6)); padding: 5px 10px; } From 5f2ab58224c3654db2ea9e8fbdff270c7036ea8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Jan 2025 10:52:38 +0100 Subject: [PATCH 45/78] Remove superfluous div --- resources/css/bem/daily-challenge.less | 9 ++---- resources/js/profile-page/daily-challenge.tsx | 30 +++++++++---------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/resources/css/bem/daily-challenge.less b/resources/css/bem/daily-challenge.less index e3e97b527c4..ecee003191d 100644 --- a/resources/css/bem/daily-challenge.less +++ b/resources/css/bem/daily-challenge.less @@ -6,6 +6,9 @@ border-radius: @border-radius-large; min-width: 0; position: relative; + display: flex; + align-items: center; + padding: 3px; &--played-today { border: 2px solid @osu-colour-lime-1; @@ -23,12 +26,6 @@ } } - &__content { - display: flex; - align-items: center; - padding: 3px; - } - &__name { font-size: @font-size--normal; padding: 0 5px; diff --git a/resources/js/profile-page/daily-challenge.tsx b/resources/js/profile-page/daily-challenge.tsx index 79d5a511704..73c380bf9ec 100644 --- a/resources/js/profile-page/daily-challenge.tsx +++ b/resources/js/profile-page/daily-challenge.tsx @@ -131,22 +131,20 @@ export default class DailyChallenge extends React.Component { className={classWithModifiers('daily-challenge', { 'played-today': playedToday })} onMouseOver={this.onMouseOver} > -
-
- {trans('users.show.daily_challenge.title').split('\\n').map((line, i) => ( -
{line}
- ))} -
-
-
- {trans( - 'users.show.daily_challenge.unit.day', - { value: formatNumber(this.props.stats.playcount) }, - )} -
+
+ {trans('users.show.daily_challenge.title').split('\\n').map((line, i) => ( +
{line}
+ ))} +
+
+
+ {trans( + 'users.show.daily_challenge.unit.day', + { value: formatNumber(this.props.stats.playcount) }, + )}
From 639c29ba3f2194255c1b251d2dc9f2fa83f8dffc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:37:48 +0000 Subject: [PATCH 46/78] Bump nesbot/carbon from 2.72.5 to 2.72.6 Bumps [nesbot/carbon](https://github.com/CarbonPHP/carbon) from 2.72.5 to 2.72.6. - [Release notes](https://github.com/CarbonPHP/carbon/releases) - [Commits](https://github.com/CarbonPHP/carbon/compare/2.72.5...2.72.6) --- updated-dependencies: - dependency-name: nesbot/carbon dependency-type: indirect ... Signed-off-by: dependabot[bot] --- composer.lock | 134 +++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/composer.lock b/composer.lock index a5f11026f9f..97ce1ad5739 100644 --- a/composer.lock +++ b/composer.lock @@ -5060,16 +5060,16 @@ }, { "name": "nesbot/carbon", - "version": "2.72.5", + "version": "2.72.6", "source": { "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "afd46589c216118ecd48ff2b95d77596af1e57ed" + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "1e9d50601e7035a4c61441a208cb5bed73e108c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/afd46589c216118ecd48ff2b95d77596af1e57ed", - "reference": "afd46589c216118ecd48ff2b95d77596af1e57ed", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1e9d50601e7035a4c61441a208cb5bed73e108c5", + "reference": "1e9d50601e7035a4c61441a208cb5bed73e108c5", "shasum": "" }, "require": { @@ -5089,7 +5089,7 @@ "doctrine/orm": "^2.7 || ^3.0", "friendsofphp/php-cs-fixer": "^3.0", "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "*", + "ondrejmirtes/better-reflection": "<6", "phpmd/phpmd": "^2.9", "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^0.12.99 || ^1.7.14", @@ -5102,10 +5102,6 @@ ], "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev" - }, "laravel": { "providers": [ "Carbon\\Laravel\\ServiceProvider" @@ -5115,6 +5111,10 @@ "includes": [ "extension.neon" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" } }, "autoload": { @@ -5163,7 +5163,7 @@ "type": "tidelift" } ], - "time": "2024-06-03T19:18:41+00:00" + "time": "2024-12-27T09:28:11+00:00" }, { "name": "nette/schema", @@ -8073,16 +8073,16 @@ }, { "name": "symfony/cache-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/cache-contracts.git", - "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197" + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/df6a1a44c890faded49a5fca33c2d5c5fd3c2197", - "reference": "df6a1a44c890faded49a5fca33c2d5c5fd3c2197", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", + "reference": "15a4f8e5cd3bce9aeafc882b1acab39ec8de2c1b", "shasum": "" }, "require": { @@ -8091,12 +8091,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -8129,7 +8129,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/cache-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/cache-contracts/tree/v3.5.1" }, "funding": [ { @@ -8145,7 +8145,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/console", @@ -8325,12 +8325,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -8530,16 +8530,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", - "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", + "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", "shasum": "" }, "require": { @@ -8548,12 +8548,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -8586,7 +8586,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" }, "funding": [ { @@ -8602,7 +8602,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/finder", @@ -8763,16 +8763,16 @@ }, { "name": "symfony/http-client-contracts", - "version": "v3.5.0", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ee8d807ab20fcb51267fdace50fbe3494c31e645", + "reference": "ee8d807ab20fcb51267fdace50fbe3494c31e645", "shasum": "" }, "require": { @@ -8780,12 +8780,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -8821,7 +8821,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.2" }, "funding": [ { @@ -8837,7 +8837,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-12-07T08:49:48+00:00" }, { "name": "symfony/http-foundation", @@ -9609,8 +9609,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -10133,16 +10133,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", - "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", "shasum": "" }, "require": { @@ -10155,12 +10155,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -10196,7 +10196,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" }, "funding": [ { @@ -10212,7 +10212,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/string", @@ -10398,16 +10398,16 @@ }, { "name": "symfony/translation-contracts", - "version": "v3.5.0", + "version": "v3.5.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", - "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c", + "reference": "4667ff3bd513750603a09c8dedbea942487fb07c", "shasum": "" }, "require": { @@ -10415,12 +10415,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -10456,7 +10456,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1" }, "funding": [ { @@ -10472,7 +10472,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-09-25T14:20:29+00:00" }, { "name": "symfony/uid", From fdf2e5822ab9a4b9d5edbaa0bb29e8395d684a74 Mon Sep 17 00:00:00 2001 From: nanaya Date: Fri, 10 Jan 2025 17:47:32 +0900 Subject: [PATCH 47/78] Fix env file setup for dusk.local Also fixed bug of github token being echoed to console if set. Note that the APP_ENV in .env.dusk.local isn't actually used when using docker compose as it overrides the value with the one in .env. --- docker/development/prepare.sh | 41 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/docker/development/prepare.sh b/docker/development/prepare.sh index 7f69ea08701..710db995174 100755 --- a/docker/development/prepare.sh +++ b/docker/development/prepare.sh @@ -15,18 +15,24 @@ _run() { docker compose run --rm php "$@" } -_run_dusk() { - docker compose run --rm -e APP_ENV=dusk.local php "$@" -} - -if [ ! -f .env ]; then - echo "Copying default env file" - cp .env.example .env -fi +for envfile in .env .env.testing .env.dusk.local; do + if [ ! -f "${envfile}" ]; then + echo "Copying env file '${envfile}'" + cp "${envfile}.example" "${envfile}" + fi + if [ "${envfile}" != .env.testing ] && ! grep -q '^APP_KEY=.' "${envfile}"; then + echo "Generating app key for env file '${envfile}'" + sed -i -e '/^APP_KEY=.*/d' "${envfile}" + : "${APP_KEY="base64:$(head -c 32 /dev/urandom | base64)"}" + echo "APP_KEY=${APP_KEY}" >> "${envfile}" + fi +done if [ -n "${GITHUB_TOKEN:-}" ]; then _run composer config -g github-oauth.github.com "${GITHUB_TOKEN}" - grep ^GITHUB_TOKEN= .env || echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> .env + for envfile in .env .env.dusk.local; do + grep -q '^GITHUB_TOKEN=' "${envfile}" || echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> "${envfile}" + done fi docker compose build @@ -37,23 +43,6 @@ _run composer install _run artisan dusk:chrome-driver -if ! grep -q '^APP_KEY=.' .env; then - echo "Generating app key" - _run artisan key:generate -fi - -if [ ! -f .env.testing ]; then - echo "Copying default test env file" - cp .env.testing.example .env.testing -fi - -if [ ! -f .env.dusk.local ]; then - echo "Copying default dusk env file" - cp .env.dusk.local.example .env.dusk.local - echo "Generating app key for dusk" - _run_dusk artisan key:generate -fi - if [ -d storage/oauth-public.key ]; then echo "oauth-public.key is a directory. Removing it" rmdir storage/oauth-public.key From 298f2acea9e9d80c0e9faf62ab702e09a3c68185 Mon Sep 17 00:00:00 2001 From: nanaya Date: Fri, 10 Jan 2025 19:04:52 +0900 Subject: [PATCH 48/78] Quotes optional --- docker/development/prepare.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/development/prepare.sh b/docker/development/prepare.sh index 710db995174..94ba4806019 100755 --- a/docker/development/prepare.sh +++ b/docker/development/prepare.sh @@ -23,7 +23,7 @@ for envfile in .env .env.testing .env.dusk.local; do if [ "${envfile}" != .env.testing ] && ! grep -q '^APP_KEY=.' "${envfile}"; then echo "Generating app key for env file '${envfile}'" sed -i -e '/^APP_KEY=.*/d' "${envfile}" - : "${APP_KEY="base64:$(head -c 32 /dev/urandom | base64)"}" + : ${APP_KEY="base64:$(head -c 32 /dev/urandom | base64)"} echo "APP_KEY=${APP_KEY}" >> "${envfile}" fi done From d0be2dc1c67aca7e5b7b8d1c98cb3db3069725e4 Mon Sep 17 00:00:00 2001 From: nanaya Date: Fri, 10 Jan 2025 22:27:57 +0900 Subject: [PATCH 49/78] Add interop endpoint to create and join multiplayer room --- .../InterOp/Multiplayer/RoomsController.php | 35 ++++++ .../Multiplayer/RoomsController.php | 47 ++------ app/Models/Multiplayer/Room.php | 12 +++ .../Multiplayer/RoomTransformer.php | 20 ++++ app/helpers.php | 8 +- routes/web.php | 5 + .../Multiplayer/RoomsControllerTest.php | 101 ++++++++++++++++++ tests/TestCase.php | 15 ++- 8 files changed, 200 insertions(+), 43 deletions(-) create mode 100644 app/Http/Controllers/InterOp/Multiplayer/RoomsController.php create mode 100644 tests/Controllers/InterOp/Multiplayer/RoomsControllerTest.php diff --git a/app/Http/Controllers/InterOp/Multiplayer/RoomsController.php b/app/Http/Controllers/InterOp/Multiplayer/RoomsController.php new file mode 100644 index 00000000000..27e08dfbb9c --- /dev/null +++ b/app/Http/Controllers/InterOp/Multiplayer/RoomsController.php @@ -0,0 +1,35 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +namespace App\Http\Controllers\InterOp\Multiplayer; + +use App\Http\Controllers\Controller; +use App\Models\Multiplayer\Room; +use App\Models\User; +use App\Transformers\Multiplayer\RoomTransformer; + +class RoomsController extends Controller +{ + public function join(string $id, string $userId) + { + $user = User::findOrFail($userId); + $room = Room::findOrFail($id); + + $room->assertCorrectPassword(get_string(request('password'))); + $room->join($user); + + return RoomTransformer::createShowResponse($room); + } + + public function store() + { + $params = \Request::all(); + $user = User::findOrFail(get_int($params['user_id'] ?? null)); + + $room = (new Room())->startGame($user, $params); + + return RoomTransformer::createShowResponse($room); + } +} diff --git a/app/Http/Controllers/Multiplayer/RoomsController.php b/app/Http/Controllers/Multiplayer/RoomsController.php index 97c6e7fd555..00d0bdd7c13 100644 --- a/app/Http/Controllers/Multiplayer/RoomsController.php +++ b/app/Http/Controllers/Multiplayer/RoomsController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers\Multiplayer; -use App\Exceptions\InvariantException; use App\Http\Controllers\Controller; use App\Http\Controllers\Ranking\DailyChallengeController; use App\Models\Model; @@ -101,24 +100,18 @@ public function index() public function join($roomId, $userId) { + $currentUser = \Auth::user(); // this allows admins/whatever to add users to games in the future - if (get_int($userId) !== auth()->user()->user_id) { + if (get_int($userId) !== $currentUser->getKey()) { abort(403); } $room = Room::findOrFail($roomId); + $room->assertCorrectPassword(get_string(request('password'))); - if ($room->password !== null) { - $password = get_param_value(request('password'), null); - - if ($password === null || !hash_equals(hash('sha256', $room->password), hash('sha256', $password))) { - abort(403, osu_trans('multiplayer.room.invalid_password')); - } - } - - $room->join(auth()->user()); + $room->join($currentUser); - return $this->createJoinedRoomResponse($room); + return RoomTransformer::createShowResponse($room); } public function leaderboard($roomId) @@ -168,7 +161,7 @@ public function show($id) } if (is_api_request()) { - return $this->createJoinedRoomResponse($room); + return RoomTransformer::createShowResponse($room); } if ($room->category === 'daily_challenge') { @@ -200,32 +193,8 @@ public function show($id) public function store() { - try { - $room = (new Room())->startGame(auth()->user(), request()->all()); - - return $this->createJoinedRoomResponse($room); - } catch (InvariantException $e) { - return error_popup($e->getMessage(), $e->getStatusCode()); - } - } + $room = (new Room())->startGame(\Auth::user(), \Request::all()); - private function createJoinedRoomResponse($room) - { - return json_item( - $room->loadMissing([ - 'host', - 'playlist.beatmap.beatmapset', - 'playlist.beatmap.baseMaxCombo', - ]), - 'Multiplayer\Room', - [ - 'current_user_score.playlist_item_attempts', - 'host.country', - 'playlist.beatmap.beatmapset', - 'playlist.beatmap.checksum', - 'playlist.beatmap.max_combo', - 'recent_participants', - ] - ); + return RoomTransformer::createShowResponse($room); } } diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index c50ed5d63e3..a4aaedb1f82 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -6,6 +6,7 @@ namespace App\Models\Multiplayer; use App\Casts\PresentString; +use App\Exceptions\AuthorizationException; use App\Exceptions\InvariantException; use App\Models\Beatmap; use App\Models\Chat\Channel; @@ -332,6 +333,17 @@ public function scopeWithRecentParticipantIds($query, ?int $limit = null) ", 'recent_participant_ids'); } + public function assertCorrectPassword(?string $password): void + { + if ($this->password === null) { + return; + } + + if ($password === null || !hash_equals(hash('sha256', $this->password), hash('sha256', $password))) { + throw new AuthorizationException(osu_trans('multiplayer.room.invalid_password')); + } + } + public function difficultyRange() { $extraQuery = true; diff --git a/app/Transformers/Multiplayer/RoomTransformer.php b/app/Transformers/Multiplayer/RoomTransformer.php index c96cae14b56..cc3d0cf93c1 100644 --- a/app/Transformers/Multiplayer/RoomTransformer.php +++ b/app/Transformers/Multiplayer/RoomTransformer.php @@ -23,6 +23,26 @@ class RoomTransformer extends TransformerAbstract 'recent_participants', ]; + public static function createShowResponse(Room $room): array + { + return json_item( + $room->loadMissing([ + 'host', + 'playlist.beatmap.baseMaxCombo', + 'playlist.beatmap.beatmapset', + ]), + new static(), + [ + 'current_user_score.playlist_item_attempts', + 'host.country', + 'playlist.beatmap.beatmapset', + 'playlist.beatmap.checksum', + 'playlist.beatmap.max_combo', + 'recent_participants', + ], + ); + } + public function transform(Room $room) { return [ diff --git a/app/helpers.php b/app/helpers.php index 81685d9c2bc..2a808374167 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -840,7 +840,9 @@ function forum_user_link(int $id, string $username, string|null $colour, int|nul function is_api_request(): bool { - return str_starts_with(rawurldecode(Request::getPathInfo()), '/api/'); + $url = rawurldecode(Request::getPathInfo()); + return str_starts_with($url, '/api/') + || str_starts_with($url, '/_lio/'); } function is_http(string $url): bool @@ -1718,6 +1720,10 @@ function parse_time_to_carbon($value) if ($value instanceof DateTime) { return Carbon\Carbon::instance($value); } + + if ($value instanceof Carbon\CarbonImmutable) { + return $value->toMutable(); + } } function format_duration_for_display(int $seconds) diff --git a/routes/web.php b/routes/web.php index 2dc8ccb6689..6437bcae2d1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -602,6 +602,11 @@ Route::apiResource('bulk', 'Indexing\BulkController', ['only' => ['store']]); }); + Route::group(['as' => 'multiplayer.', 'namespace' => 'Multiplayer', 'prefix' => 'multiplayer'], function () { + Route::put('rooms/{room}/users/{user}', 'RoomsController@join')->name('rooms.join'); + Route::apiResource('rooms', 'RoomsController', ['only' => ['store']]); + }); + Route::post('user-achievement/{user}/{achievement}/{beatmap?}', 'UsersController@achievement')->name('users.achievement'); Route::group(['as' => 'user-group.'], function () { diff --git a/tests/Controllers/InterOp/Multiplayer/RoomsControllerTest.php b/tests/Controllers/InterOp/Multiplayer/RoomsControllerTest.php new file mode 100644 index 00000000000..353e1902eea --- /dev/null +++ b/tests/Controllers/InterOp/Multiplayer/RoomsControllerTest.php @@ -0,0 +1,101 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +namespace Tests\Controllers\InterOp\Multiplayer; + +use App\Models\Beatmap; +use App\Models\Chat\UserChannel; +use App\Models\Multiplayer\Room; +use App\Models\User; +use Carbon\CarbonImmutable; +use Tests\TestCase; + +class RoomsControllerTest extends TestCase +{ + private static function startRoomParams(): array + { + $beatmap = Beatmap::factory()->create(); + + return [ + 'ends_at' => CarbonImmutable::now()->addHours(1), + 'name' => 'test room', + 'type' => Room::REALTIME_DEFAULT_TYPE, + 'playlist' => [[ + 'beatmap_id' => $beatmap->getKey(), + 'ruleset_id' => $beatmap->playmode, + ]], + ]; + } + + public function testJoin(): void + { + $room = (new Room())->startGame(User::factory()->create(), static::startRoomParams()); + $user = User::factory()->create(); + + $this->expectCountChange(fn () => UserChannel::count(), 1); + + $this->withInterOpHeader( + route('interop.multiplayer.rooms.join', [ + 'room' => $room->getKey(), + 'user' => $user->getKey(), + ]), + fn ($url) => $this->put($url), + )->assertSuccessful(); + } + + public function testJoinWithPassword(): void + { + $room = (new Room())->startGame(User::factory()->create(), [ + ...static::startRoomParams(), + 'password' => 'hunter2', + ]); + $user = User::factory()->create(); + + $this->expectCountChange(fn () => UserChannel::count(), 1); + + $this->withInterOpHeader( + route('interop.multiplayer.rooms.join', [ + 'room' => $room->getKey(), + 'user' => $user->getKey(), + ]), + fn ($url) => $this->put($url, ['password' => 'hunter2']), + )->assertSuccessful(); + } + + public function testJoinWithPasswordInvalid(): void + { + $room = (new Room())->startGame(User::factory()->create(), [ + ...static::startRoomParams(), + 'password' => 'hunter2', + ]); + $user = User::factory()->create(); + + $this->expectCountChange(fn () => UserChannel::count(), 0); + + $this->withInterOpHeader( + route('interop.multiplayer.rooms.join', [ + 'room' => $room->getKey(), + 'user' => $user->getKey(), + ]), + fn ($url) => $this->put($url, ['password' => '*******']), + )->assertStatus(403); + } + + public function testStore(): void + { + $beatmap = Beatmap::factory()->create(); + $params = [ + ...static::startRoomParams(), + 'user_id' => User::factory()->create()->getKey(), + ]; + + $this->expectCountChange(fn () => Room::count(), 1); + + $this->withInterOpHeader( + route('interop.multiplayer.rooms.store'), + fn ($url) => $this->post($url, $params), + )->assertSuccessful(); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 3617fbd4993..f706a8427aa 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -365,11 +365,20 @@ protected function runFakeQueue() $this->invokeSetProperty(app('queue'), 'jobs', []); } - protected function withInterOpHeader($url) + protected function withInterOpHeader($url, ?callable $callback = null) { - return $this->withHeaders([ - 'X-LIO-Signature' => hash_hmac('sha1', $url, $GLOBALS['cfg']['osu']['legacy']['shared_interop_secret']), + if ($callback === null) { + $timestampedUrl = $url; + } else { + $connector = strpos($url, '?') === false ? '?' : '&'; + $timestampedUrl = $url.$connector.'timestamp='.time(); + } + + $this->withHeaders([ + 'X-LIO-Signature' => hash_hmac('sha1', $timestampedUrl, $GLOBALS['cfg']['osu']['legacy']['shared_interop_secret']), ]); + + return $callback === null ? $this : $callback($timestampedUrl); } protected function withPersistentSession(SessionStore $session): static From 73162ed460fa34e3bd06f6f628fc2384f6fa8fc2 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 14 Jan 2025 13:09:01 +0900 Subject: [PATCH 50/78] already a copy --- resources/js/beatmapsets-show/controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index a48de73ff41..b7651e70e9a 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -104,7 +104,7 @@ export default class Controller { const tag = asTagJsonWithCount(maybeTag); tag.count = tagId.count; - userTags.push(asTagJsonWithCount(tag)); + userTags.push(tag); } } From 585fa90fbf894ef3c2898f8a1cee4e99e0fd42af Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 14 Jan 2025 14:40:27 +0900 Subject: [PATCH 51/78] tags not being deduped anymore --- resources/js/beatmapsets-show/info.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/beatmapsets-show/info.tsx b/resources/js/beatmapsets-show/info.tsx index b5e3eadb3e1..dcbb8e30f3d 100644 --- a/resources/js/beatmapsets-show/info.tsx +++ b/resources/js/beatmapsets-show/info.tsx @@ -202,8 +202,8 @@ export default class Info extends React.Component { {trans('beatmapsets.show.info.tags')}
- {this.tags.map((tag) => ( - + {this.tags.map((tag, i) => ( + Date: Tue, 14 Jan 2025 15:28:34 +0900 Subject: [PATCH 52/78] fix property ordering --- app/Models/BeatmapTag.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/BeatmapTag.php b/app/Models/BeatmapTag.php index fc7abf63ce8..c83f36a0f8d 100644 --- a/app/Models/BeatmapTag.php +++ b/app/Models/BeatmapTag.php @@ -18,9 +18,10 @@ */ class BeatmapTag extends Model { + public $incrementing = false; + protected $primaryKey = ':composite'; protected $primaryKeys = ['beatmap_id', 'tag_id', 'user_id']; - public $incrementing = false; public static function topTagIdsQuery(int $beatmapId, int $limit = 50) { From be4740055755b2ccca8440b7fbeea498ba9eb245 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 14 Jan 2025 15:31:19 +0900 Subject: [PATCH 53/78] only used in one place now... --- resources/js/beatmapsets-show/controller.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/resources/js/beatmapsets-show/controller.ts b/resources/js/beatmapsets-show/controller.ts index b7651e70e9a..8216dd4c523 100644 --- a/resources/js/beatmapsets-show/controller.ts +++ b/resources/js/beatmapsets-show/controller.ts @@ -27,14 +27,6 @@ interface State { type TagJsonWithCount = TagJson & { count: number }; -function asTagJsonWithCount(tag: TagJson) { - return { - count: 0, - ...tag, - }; -} - - export default class Controller { @observable hoveredBeatmap: null | BeatmapJsonForBeatmapsetShow = null; @observable state: State; @@ -102,9 +94,7 @@ export default class Controller { const maybeTag = this.relatedTags.get(tagId.tag_id); if (maybeTag == null) continue; - const tag = asTagJsonWithCount(maybeTag); - tag.count = tagId.count; - userTags.push(tag); + userTags.push({ ...maybeTag, count: tagId.count } ); } } From 3dee3fcb29c85ddc9ab4e77374640785ec6a5ada Mon Sep 17 00:00:00 2001 From: bakaneko Date: Tue, 14 Jan 2025 16:46:40 +0900 Subject: [PATCH 54/78] can be a scope? --- app/Models/Beatmap.php | 2 +- app/Models/BeatmapTag.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Models/Beatmap.php b/app/Models/Beatmap.php index 3cca72263e3..9bda60e088d 100644 --- a/app/Models/Beatmap.php +++ b/app/Models/Beatmap.php @@ -358,7 +358,7 @@ public function topTagIds() "beatmap_top_tag_ids:{$this->getKey()}", $GLOBALS['cfg']['osu']['tags']['beatmap_tags_cache_duration'], [], - fn () => BeatmapTag::topTagIdsQuery($this->getKey())->get()->toArray(), + fn () => $this->beatmapTags()->topTagIds()->limit(50)->get()->toArray(), ), ); } diff --git a/app/Models/BeatmapTag.php b/app/Models/BeatmapTag.php index c83f36a0f8d..a61da0fc571 100644 --- a/app/Models/BeatmapTag.php +++ b/app/Models/BeatmapTag.php @@ -7,6 +7,8 @@ namespace App\Models; +use Illuminate\Contracts\Database\Eloquent\Builder; + /** * @property-read Beatmap $beatmap * @property int $beatmap_id @@ -23,16 +25,14 @@ class BeatmapTag extends Model protected $primaryKey = ':composite'; protected $primaryKeys = ['beatmap_id', 'tag_id', 'user_id']; - public static function topTagIdsQuery(int $beatmapId, int $limit = 50) + public function scopeTopTagIds(Builder $query) { - return static::where('beatmap_id', $beatmapId) - ->whereHas('user', fn ($userQuery) => $userQuery->default()) + return $query->whereHas('user', fn ($userQuery) => $userQuery->default()) ->groupBy('tag_id') ->select('tag_id') ->selectRaw('COUNT(*) as count') ->orderBy('count', 'desc') - ->orderBy('tag_id', 'asc') - ->limit($limit); + ->orderBy('tag_id', 'asc'); } public function beatmap() From 12fa440666e7e75d92f1a39319f0a4372259afb2 Mon Sep 17 00:00:00 2001 From: AJ Granowski Date: Wed, 15 Jan 2025 03:17:07 -0600 Subject: [PATCH 55/78] Have octane wait for a manifest to exist PR: #11779 --- docker/development/run.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docker/development/run.sh b/docker/development/run.sh index 372b5991eb7..4fd8d6eadc5 100755 --- a/docker/development/run.sh +++ b/docker/development/run.sh @@ -20,7 +20,19 @@ _migrate() { } _octane() { - exec ./artisan octane:start --host=0.0.0.0 "$@" + echo_counter=0 + manifest_path='./public/assets/manifest.json' + while [ ! -f "$manifest_path" ]; do + if [ "$echo_counter" -le 0 ]; then + echo_counter=5 + echo "waiting to start octane: ${manifest_path##*/} not found" >&2 + fi + + : $(( echo_counter -= 1 )) + sleep 1 + done + + exec ./artisan octane:start --host=0.0.0.0 "$@" } _schedule() { From 87db8f489caf8e2f153d08f7a68d2f8cb3f862f1 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 15 Jan 2025 21:29:32 +0900 Subject: [PATCH 56/78] Readd beatmap_id validation Ruleset id validation is done in ScoreToken. --- app/Models/Multiplayer/Room.php | 37 ++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 30041b14c4b..8dcf6d088ed 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -662,9 +662,21 @@ public function startPlay(User $user, PlaylistItem $playlistItem, array $rawPara { priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan(); - $this->assertValidStartPlay($user, $playlistItem); + $params = get_params($rawParams, null, [ + 'beatmap_hash', + 'beatmap_id:int', + 'build_id', + 'ruleset_id:int', + ], ['null_missing' => true]); + + if (!$playlistItem->freestyle) { + $params['beatmap_id'] = $playlistItem->beatmap_id; + $params['ruleset_id'] = $playlistItem->ruleset_id; + } - return $this->getConnection()->transaction(function () use ($playlistItem, $rawParams, $user) { + $this->assertValidStartPlay($user, $playlistItem, $params); + + return $this->getConnection()->transaction(function () use ($params, $playlistItem, $user) { $agg = UserScoreAggregate::new($user, $this); if ($agg->wasRecentlyCreated) { $this->incrementInstance('participant_count'); @@ -676,11 +688,11 @@ public function startPlay(User $user, PlaylistItem $playlistItem, array $rawPara $playlistItemAgg->updateUserAttempts(); return ScoreToken::create([ - 'beatmap_hash' => get_string($rawParams['beatmap_hash'] ?? null), - 'beatmap_id' => $playlistItem->beatmap_id, - 'build_id' => $rawParams['build_id'], + 'beatmap_hash' => $params['beatmap_hash'], + 'beatmap_id' => $params['beatmap_id'], + 'build_id' => $params['build_id'], 'playlist_item_id' => $playlistItem->getKey(), - 'ruleset_id' => $playlistItem->ruleset_id, + 'ruleset_id' => $params['ruleset_id'], 'user_id' => $user->getKey(), ]); }); @@ -741,7 +753,7 @@ private function assertValidStartGame() } } - private function assertValidStartPlay(User $user, PlaylistItem $playlistItem) + private function assertValidStartPlay(User $user, PlaylistItem $playlistItem, array $params): void { // todo: check against room's end time (to see if player has enough time to play this beatmap) and is under the room's max attempts limit @@ -749,6 +761,17 @@ private function assertValidStartPlay(User $user, PlaylistItem $playlistItem) throw new InvariantException('Room has already ended.'); } + if ($playlistItem->freestyle) { + // assert the beatmap_id is part of playlist item's beatmapset + $beatmapsetIdCount = Beatmap + ::whereKey([$playlistItem->beatmap_id, $params['beatmap_id']]) + ->distinct('beatmapset_id') + ->count(); + if ($beatmapsetIdCount !== 1) { + throw new InvariantException('Specified beatmap_id is not allowed'); + } + } + $userId = $user->getKey(); if ($this->max_attempts !== null) { $roomStats = $this->userHighScores()->where('user_id', $userId)->first(); From 0d393a8e1f343eeea4abc457b8eddc8d91d10840 Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 15 Jan 2025 21:34:52 +0900 Subject: [PATCH 57/78] Disallow mod on freestyle --- app/Models/Multiplayer/PlaylistItem.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Models/Multiplayer/PlaylistItem.php b/app/Models/Multiplayer/PlaylistItem.php index b8ab7d8b56b..37ca9b1a653 100644 --- a/app/Models/Multiplayer/PlaylistItem.php +++ b/app/Models/Multiplayer/PlaylistItem.php @@ -171,6 +171,10 @@ private function assertValidRuleset() private function assertValidMods() { + if ($this->freestyle && (count($this->allowed_mods) === 0 || count($this->required_mods) === 0)) { + throw new InvariantException("mod isn't allowed in freestyle"); + } + $allowedModIds = array_column($this->allowed_mods, 'acronym'); $requiredModIds = array_column($this->required_mods, 'acronym'); From adecc03f8e59ef424221b9b6f31bd1c52943385e Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 15 Jan 2025 21:37:44 +0900 Subject: [PATCH 58/78] Lint fix --- .../Controllers/Multiplayer/Rooms/Playlist/ScoresController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php index 9dd6d4ce9ea..19b79dde254 100644 --- a/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php +++ b/app/Http/Controllers/Multiplayer/Rooms/Playlist/ScoresController.php @@ -7,7 +7,6 @@ use App\Http\Controllers\Controller as BaseController; use App\Libraries\ClientCheck; -use App\Models\Beatmap; use App\Models\Multiplayer\PlaylistItem; use App\Models\Multiplayer\PlaylistItemUserHighScore; use App\Models\Multiplayer\Room; From ac1b77b7ee6ef2ddc2be44aab23574e26b30d83e Mon Sep 17 00:00:00 2001 From: nanaya Date: Wed, 15 Jan 2025 21:42:04 +0900 Subject: [PATCH 59/78] No smarts allowed Easier to understand. --- app/Models/Multiplayer/Room.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 8dcf6d088ed..19e9d2c7d8b 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -763,11 +763,7 @@ private function assertValidStartPlay(User $user, PlaylistItem $playlistItem, ar if ($playlistItem->freestyle) { // assert the beatmap_id is part of playlist item's beatmapset - $beatmapsetIdCount = Beatmap - ::whereKey([$playlistItem->beatmap_id, $params['beatmap_id']]) - ->distinct('beatmapset_id') - ->count(); - if ($beatmapsetIdCount !== 1) { + if ($playlistItem->beatmap->beatmapset_id !== Beatmap::find($params['beatmap_id'])?->getKey()) { throw new InvariantException('Specified beatmap_id is not allowed'); } } From e69ab6ad6c50f3eafeecc9dcb50c296e76b50c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Jan 2025 14:47:25 +0100 Subject: [PATCH 60/78] Apply review suggestions --- resources/css/bem/daily-challenge.less | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/css/bem/daily-challenge.less b/resources/css/bem/daily-challenge.less index ecee003191d..0f95ff5d90a 100644 --- a/resources/css/bem/daily-challenge.less +++ b/resources/css/bem/daily-challenge.less @@ -8,10 +8,11 @@ position: relative; display: flex; align-items: center; - padding: 3px; + padding: 1px; + border: 2px solid transparent; &--played-today { - border: 2px solid @osu-colour-lime-1; + border-color: @osu-colour-lime-1; &::before { .fas(); @@ -19,10 +20,11 @@ border-radius: 50%; color: @osu-colour-lime-1; content: @fa-var-check-circle; - font-size: 16px; + font-size: 16px; // icon size position: absolute; - right: -8px; - top: -8px; + right: 0; + top: 0; + transform: translate(50%, -50%); } } From 2b05d0980cd8ce3655ebd41c9c2624425f5ad479 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 16 Jan 2025 15:34:58 +0900 Subject: [PATCH 61/78] Fix incorrect check and short circuit if passed --- app/Models/Multiplayer/PlaylistItem.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Models/Multiplayer/PlaylistItem.php b/app/Models/Multiplayer/PlaylistItem.php index 37ca9b1a653..4751a5eeff6 100644 --- a/app/Models/Multiplayer/PlaylistItem.php +++ b/app/Models/Multiplayer/PlaylistItem.php @@ -171,8 +171,11 @@ private function assertValidRuleset() private function assertValidMods() { - if ($this->freestyle && (count($this->allowed_mods) === 0 || count($this->required_mods) === 0)) { - throw new InvariantException("mod isn't allowed in freestyle"); + if ($this->freestyle) { + if (count($this->allowed_mods) !== 0 || count($this->required_mods) !== 0) { + throw new InvariantException("mod isn't allowed in freestyle"); + } + return; } $allowedModIds = array_column($this->allowed_mods, 'acronym'); From 6f053b369d2ba5eeb3b7cf699b3a617bbe242c15 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 16 Jan 2025 15:48:43 +0900 Subject: [PATCH 62/78] Wrong id --- app/Models/Multiplayer/Room.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 19e9d2c7d8b..06088752613 100644 --- a/app/Models/Multiplayer/Room.php +++ b/app/Models/Multiplayer/Room.php @@ -763,7 +763,7 @@ private function assertValidStartPlay(User $user, PlaylistItem $playlistItem, ar if ($playlistItem->freestyle) { // assert the beatmap_id is part of playlist item's beatmapset - if ($playlistItem->beatmap->beatmapset_id !== Beatmap::find($params['beatmap_id'])?->getKey()) { + if ($playlistItem->beatmap->beatmapset_id !== Beatmap::find($params['beatmap_id'])?->beatmapset_id) { throw new InvariantException('Specified beatmap_id is not allowed'); } } From 954dc28bd74da48b9b7affe0becaa19a854576ec Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 16 Jan 2025 18:26:05 +0900 Subject: [PATCH 63/78] allow selectively excluding bots and no_profile users from lookup --- .../Controllers/Users/LookupController.php | 18 +++++++++++++----- .../Beatmapset/ChangeBeatmapOwners.php | 2 +- app/Models/User.php | 9 +++++---- .../beatmap-owner-editor.tsx | 1 + resources/js/chat/create-announcement.tsx | 1 + resources/js/components/username-input.tsx | 3 ++- resources/js/store/store-supporter-tag.tsx | 2 +- resources/js/utils/user.ts | 4 ++-- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/Users/LookupController.php b/app/Http/Controllers/Users/LookupController.php index 725373624c2..f94f37bd8b0 100644 --- a/app/Http/Controllers/Users/LookupController.php +++ b/app/Http/Controllers/Users/LookupController.php @@ -22,7 +22,12 @@ public function __construct() public function index() { // TODO: referer check? - $ids = array_slice(array_reject_null(get_arr(request('ids'), presence(...)) ?? []), 0, 50); + $params = get_params(request()->all(), null, [ + 'exclude_bots:bool', + 'ids:string[]', + ]); + + $ids = array_slice(array_reject_null(get_arr($params['ids'] ?? [], presence(...))), 0, 50); $numericIds = []; $stringIds = []; @@ -35,12 +40,15 @@ public function index() } $users = User::where(fn ($q) => $q->whereIn('user_id', $numericIds)->orWhereIn('username', $stringIds)) - ->defaultForLookup() - ->with(UserCompactTransformer::CARD_INCLUDES_PRELOAD) - ->get(); + ->default() + ->with(UserCompactTransformer::CARD_INCLUDES_PRELOAD); + + if ($params['exclude_bots'] ?? false) { + $users = $users->withoutBots(); + } return [ - 'users' => json_collection($users, new UserCompactTransformer(), UserCompactTransformer::CARD_INCLUDES), + 'users' => json_collection($users->get(), new UserCompactTransformer(), UserCompactTransformer::CARD_INCLUDES), ]; } } diff --git a/app/Libraries/Beatmapset/ChangeBeatmapOwners.php b/app/Libraries/Beatmapset/ChangeBeatmapOwners.php index 5b137b9a60d..32ec991b00f 100644 --- a/app/Libraries/Beatmapset/ChangeBeatmapOwners.php +++ b/app/Libraries/Beatmapset/ChangeBeatmapOwners.php @@ -43,7 +43,7 @@ public function handle(): void $newUserIds = $this->userIds->diff($currentOwners); - if (User::whereIn('user_id', $newUserIds->toArray())->defaultForLookup()->count() !== $newUserIds->count()) { + if (User::whereIn('user_id', $newUserIds->toArray())->default()->withoutBots()->count() !== $newUserIds->count()) { throw new InvariantException('invalid user_id'); } diff --git a/app/Models/User.php b/app/Models/User.php index 7c951737fad..916f1f624fe 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2032,13 +2032,14 @@ public function scopeDefault($query) ]); } - public function scopeDefaultForLookup(Builder $query): Builder + public function scopeWithoutBots(Builder $query): Builder { $groups = app('groups'); - return $query - ->whereNotIn('group_id', [$groups->byIdentifier('no_profile')->getKey(), $groups->byIdentifier('bot')->getKey()]) - ->default(); + return $query->whereNotIn( + 'group_id', + [$groups->byIdentifier('no_profile')->getKey(), $groups->byIdentifier('bot')->getKey()], + ); } public function scopeOnline($query) diff --git a/resources/js/beatmap-discussions/beatmap-owner-editor.tsx b/resources/js/beatmap-discussions/beatmap-owner-editor.tsx index d071c24c6c3..f6d616000f1 100644 --- a/resources/js/beatmap-discussions/beatmap-owner-editor.tsx +++ b/resources/js/beatmap-discussions/beatmap-owner-editor.tsx @@ -189,6 +189,7 @@ export default class BeatmapOwnerEditor extends React.Component {
{this.editing ? ( {
{ } try { - this.xhr = apiLookupUsers(userIds); + this.xhr = apiLookupUsers(userIds, this.props.excludeBots); const response = await this.xhr; this.extractValidUsers(response.users); } catch (error) { diff --git a/resources/js/store/store-supporter-tag.tsx b/resources/js/store/store-supporter-tag.tsx index 5e39f229e90..4db15f63c38 100644 --- a/resources/js/store/store-supporter-tag.tsx +++ b/resources/js/store/store-supporter-tag.tsx @@ -224,7 +224,7 @@ export default class StoreSupporterTag extends React.Component { @action private readonly getUser = (username: string) => { - this.xhr = apiLookupUsers([`@${username}`]); + this.xhr = apiLookupUsers([`@${username}`], true); this.xhr .done((response) => runInAction(() => { diff --git a/resources/js/utils/user.ts b/resources/js/utils/user.ts index a403709d51f..1345ac81dee 100644 --- a/resources/js/utils/user.ts +++ b/resources/js/utils/user.ts @@ -4,9 +4,9 @@ import UserJson from 'interfaces/user-json'; import { route } from 'laroute'; -export function apiLookupUsers(idsOrUsernames: (string | null | undefined)[]) { +export function apiLookupUsers(idsOrUsernames: (string | null | undefined)[], excludeBots?: boolean) { return $.ajax(route('users.lookup'), { - data: { ids: idsOrUsernames }, + data: { exclude_bots: excludeBots, ids: idsOrUsernames }, dataType: 'json', }) as JQuery.jqXHR<{ users: UserJson[] }>; } From fd749b56ddd9428d2e8f56f148eb956e75cb2e79 Mon Sep 17 00:00:00 2001 From: bakaneko Date: Thu, 16 Jan 2025 19:49:46 +0900 Subject: [PATCH 64/78] use Request facade instead --- app/Http/Controllers/Users/LookupController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Users/LookupController.php b/app/Http/Controllers/Users/LookupController.php index f94f37bd8b0..0b22521f6ca 100644 --- a/app/Http/Controllers/Users/LookupController.php +++ b/app/Http/Controllers/Users/LookupController.php @@ -22,7 +22,7 @@ public function __construct() public function index() { // TODO: referer check? - $params = get_params(request()->all(), null, [ + $params = get_params(\Request::all(), null, [ 'exclude_bots:bool', 'ids:string[]', ]); From 82fc1122df27ef1b39d19375f44c71fe049883a7 Mon Sep 17 00:00:00 2001 From: nanaya Date: Thu, 16 Jan 2025 19:55:18 +0900 Subject: [PATCH 65/78] Remove proxying option of blade background image --- app/helpers.php | 12 ++++-------- resources/views/follows/modding.blade.php | 2 +- resources/views/forum/topics/_post_info.blade.php | 2 +- resources/views/layout/_header_user.blade.php | 2 +- resources/views/layout/_page_header_v4.blade.php | 2 +- resources/views/layout/_popup_user.blade.php | 2 +- resources/views/objects/_flag_team.blade.php | 2 +- 7 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/helpers.php b/app/helpers.php index 81685d9c2bc..b375c176c4c 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -55,15 +55,11 @@ function atom_id(string $namespace, $id = null): string return 'tag:'.request()->getHttpHost().',2019:'.$namespace.($id === null ? '' : "/{$id}"); } -function background_image($url, $proxy = true) +function background_image($url): string { - if (!present($url)) { - return ''; - } - - $url = $proxy ? proxy_media($url) : $url; - - return sprintf(' style="background-image:url(\'%s\');" ', e($url)); + return present($url) + ? sprintf(' style="background-image:url(\'%s\');" ', e($url)) + : ''; } function beatmap_timestamp_format($ms) diff --git a/resources/views/follows/modding.blade.php b/resources/views/follows/modding.blade.php index 18f677dec3b..210c646fdc2 100644 --- a/resources/views/follows/modding.blade.php +++ b/resources/views/follows/modding.blade.php @@ -60,7 +60,7 @@
beatmapset->coverURL('list'), false) !!} + {!! background_image($watch->beatmapset->coverURL('list')) !!} class="beatmapset-watches__cover" >
diff --git a/resources/views/forum/topics/_post_info.blade.php b/resources/views/forum/topics/_post_info.blade.php index db4bd562cbd..0b2cc37e46e 100644 --- a/resources/views/forum/topics/_post_info.blade.php +++ b/resources/views/forum/topics/_post_info.blade.php @@ -76,7 +76,7 @@ class="forum-post-info__row forum-post-info__row--title" logo()->url(), false) !!} + {!! background_image($team->logo()->url()) !!} >
diff --git a/resources/views/layout/_header_user.blade.php b/resources/views/layout/_header_user.blade.php index 1905100230f..7e7d2338157 100644 --- a/resources/views/layout/_header_user.blade.php +++ b/resources/views/layout/_header_user.blade.php @@ -21,6 +21,6 @@ class="{{ $class }} avatar--guest" class="{{ $class }} {{ Auth::user()->isRestricted() ? 'avatar--restricted' : '' }}" data-click-menu-target="nav2-user-popup" href="{{ route('users.show', Auth::user()) }}" - {!! background_image(Auth::user()->user_avatar, false) !!} + {!! background_image(Auth::user()->user_avatar) !!} > @endif diff --git a/resources/views/layout/_page_header_v4.blade.php b/resources/views/layout/_page_header_v4.blade.php index 963d1093f22..36d78c726da 100644 --- a/resources/views/layout/_page_header_v4.blade.php +++ b/resources/views/layout/_page_header_v4.blade.php @@ -24,7 +24,7 @@ ">
-
+