diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index b1a1b491fd0..3bd96032464 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -16,6 +16,7 @@ use App\Models\NewsPost; use App\Models\UserDonation; use App\Transformers\MenuImageTransformer; +use App\Transformers\UserCompactTransformer; use Auth; use Jenssegers\Agent\Agent; use Request; @@ -142,12 +143,11 @@ public function quickSearch() $result[$mode]['total'] = $search->count(); } - $result['user']['users'] = json_collection($searches['user']->data(), 'UserCompact', [ - 'country', - 'cover', - 'groups', - 'support_level', - ]); + $result['user']['users'] = json_collection( + $searches['user']->data(), + new UserCompactTransformer(), + [...UserCompactTransformer::CARD_INCLUDES, 'support_level'], + ); $result['beatmapset']['beatmapsets'] = json_collection($searches['beatmapset']->data(), 'Beatmapset', ['beatmaps']); } 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/ChangelogEntry.php b/app/Models/ChangelogEntry.php index 194a87238c7..4a377b2b8ef 100644 --- a/app/Models/ChangelogEntry.php +++ b/app/Models/ChangelogEntry.php @@ -39,9 +39,9 @@ class ChangelogEntry extends Model public static function convertLegacy($changelog) { - $message = $changelog->message; - $splitMessage = static::splitMessage($message); - $title = $splitMessage[0]; + $splitMessage = static::splitMessage($changelog->message); + $title = presence($splitMessage[0]); + $message = $splitMessage[1]; if ($title === null) { $title = $splitMessage[1]; @@ -65,6 +65,13 @@ public static function convertLegacy($changelog) ]); } + public static function getDisplayMessage(?string $origMessage): string + { + $split = static::splitMessage($origMessage); + + return $split[1] === null ? '' : $split[0]; + } + public static function guessCategory($data) { static $ignored = [ @@ -125,7 +132,7 @@ public static function importFromGithub($data) 'category' => static::guessCategory($data), 'created_at' => Carbon::parse($data['pull_request']['merged_at']), 'github_pull_request_id' => $data['pull_request']['number'], - 'message' => $data['pull_request']['body'], + 'message' => static::getDisplayMessage($data['pull_request']['body']), 'private' => static::isPrivate($data), 'title' => $data['pull_request']['title'], 'type' => static::guessType($data), @@ -180,21 +187,30 @@ public static function placeholder() ]); } - public static function splitMessage($message) + /** + * Returns array of message split by thematic break (`---`) + * + * The array length is always two. + * If the message is empty, both values will be null. + * If there's no thematic break, the second value will be null. + */ + public static function splitMessage($message): array { if (!present($message)) { return [null, null]; } static $separator = "\n\n---\n"; - // prepended with \n\n just in case the message starts with ---\n (blank first part). - $message = "\n\n".trim(str_replace("\r\n", "\n", $message)); - $splitPos = null_if_false(strpos($message, $separator)) ?? strlen($message); - - return [ - presence(trim(substr($message, 0, $splitPos))), - presence(trim(substr($message, $splitPos + strlen($separator)))), - ]; + // Surround with newlines to handle separator at the start/end. + $message = "\n\n".trim(strtr($message, ["\r\n" => "\n"]))."\n"; + $splitPos = strpos($message, $separator); + + return $splitPos === false + ? [trim($message), null] + : [ + trim(substr($message, 0, $splitPos)), + trim(substr($message, $splitPos + strlen($separator))), + ]; } public function builds() @@ -253,21 +269,12 @@ public function githubUrl() } } - public function publicMessageHtml() + public function messageHtml(): ?string { return $this->memoize(__FUNCTION__, function () { - $message = $this->publicMessage(); + $message = $this->message; - if ($message !== null) { - return markdown($message, 'changelog_entry'); - } - }); - } - - public function publicMessage() - { - return $this->memoize(__FUNCTION__, function () { - return static::splitMessage($this->message)[1]; + return present($message) ? markdown($message, 'changelog_entry') : null; }); } } diff --git a/app/Models/Multiplayer/Room.php b/app/Models/Multiplayer/Room.php index 06088752613..47e6dcad85e 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/Models/User.php b/app/Models/User.php index 53088b6d1a3..3c9f96e8fa4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -317,24 +317,23 @@ public function getAuthPassword() public function usernameChangeCost() { - $changesToDate = $this->usernameChangeHistory() - ->whereIn('type', ['support', 'paid']) - ->count(); + $tier = min($this->usernameChangeHistory()->paid()->count(), 5); - switch ($changesToDate) { - case 0: - return 0; - case 1: - return 8; - case 2: - return 16; - case 3: - return 32; - case 4: - return 64; - default: - return 100; + if ($tier > 1) { + $lastChange = $this->usernameChangeHistory()->paid()->last()?->timestamp; + if ($lastChange !== null) { + $tier = max($tier - $lastChange->diffInYears(Carbon::now(), false), 1); + } } + + return match ($tier) { + 0 => 0, + 1 => 8, + 2 => 16, + 3 => 32, + 4 => 64, + default => 100, + }; } public function revertUsername($type = 'revert'): UsernameChangeHistory diff --git a/app/Models/UsernameChangeHistory.php b/app/Models/UsernameChangeHistory.php index 2711b5bf326..abcdc26a5fe 100644 --- a/app/Models/UsernameChangeHistory.php +++ b/app/Models/UsernameChangeHistory.php @@ -21,6 +21,11 @@ class UsernameChangeHistory extends Model protected $table = 'osu_username_change_history'; protected $primaryKey = 'change_id'; + public function scopePaid($query) + { + $query->whereIn('type', ['support', 'paid']); // changed by support counts as paid. + } + public function scopeVisible($query) { $query->whereIn('type', ['support', 'paid', 'admin']); diff --git a/app/Transformers/ChangelogEntryTransformer.php b/app/Transformers/ChangelogEntryTransformer.php index f11f9091f63..0d190c36b24 100644 --- a/app/Transformers/ChangelogEntryTransformer.php +++ b/app/Transformers/ChangelogEntryTransformer.php @@ -39,11 +39,11 @@ public function includeGithubUser(ChangelogEntry $entry) public function includeMessage(ChangelogEntry $entry) { - return $this->primitive($entry->publicMessage()); + return $this->primitive($entry->message); } public function includeMessageHtml(ChangelogEntry $entry) { - return $this->primitive($entry->publicMessageHtml()); + return $this->primitive($entry->messageHtml()); } } 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..5960595cd21 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) @@ -840,7 +836,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 +1716,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/database/factories/UsernameChangeHistoryFactory.php b/database/factories/UsernameChangeHistoryFactory.php new file mode 100644 index 00000000000..7359c941b6c --- /dev/null +++ b/database/factories/UsernameChangeHistoryFactory.php @@ -0,0 +1,31 @@ +. 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 Database\Factories; + +use App\Models\User; +use App\Models\UsernameChangeHistory; +use Carbon\Carbon; + +class UsernameChangeHistoryFactory extends Factory +{ + protected $model = UsernameChangeHistory::class; + + public function definition(): array + { + return [ + 'timestamp' => Carbon::now(), + 'type' => 'paid', + 'user_id' => User::factory(), + + // depend on user_id; the username will be incorrect when factorying multiple names at once, + // so they should be handled separately if realistic name changes are wanted. + 'username' => fn (array $attr) => User::find($attr['user_id'])->username, + 'username_last' => fn (array $attr) => "{$attr['username']}_prev", + ]; + } +} diff --git a/resources/css/bem/beatmap-discussions-header-top.less b/resources/css/bem/beatmap-discussions-header-top.less index 3ccef7ae580..32c2a199f74 100644 --- a/resources/css/bem/beatmap-discussions-header-top.less +++ b/resources/css/bem/beatmap-discussions-header-top.less @@ -90,6 +90,7 @@ grid-area: owners; font-size: @font-size--title-small; padding: 10px 10px 0; + overflow-wrap: anywhere; } &__stats { diff --git a/resources/css/bem/team-members-manage.less b/resources/css/bem/team-members-manage.less index 9ddbb0f6544..250e0ddc726 100644 --- a/resources/css/bem/team-members-manage.less +++ b/resources/css/bem/team-members-manage.less @@ -8,7 +8,7 @@ font-size: @font-size--title-small; display: grid; gap: 2px 10px; - grid-template-columns: auto 1fr auto auto auto; + grid-template-columns: 1fr auto auto auto; &__avatar { .default-border-radius(); @@ -38,4 +38,11 @@ font-weight: 600; } } + + &__username { + display: flex; + align-items: center; + width: max-content; + gap: 10px; + } } diff --git a/resources/js/beatmap-discussions/header.tsx b/resources/js/beatmap-discussions/header.tsx index 45e7fdb7d80..7bb4de86829 100644 --- a/resources/js/beatmap-discussions/header.tsx +++ b/resources/js/beatmap-discussions/header.tsx @@ -177,14 +177,12 @@ export class Header extends React.Component {
{hasGuestOwners(this.currentBeatmap, this.beatmapset) && ( - - , - }} - pattern={trans('beatmaps.discussions.guest')} - /> - + , + }} + pattern={trans('beatmaps.discussions.guest')} + /> )}
diff --git a/resources/js/quick-search/user.tsx b/resources/js/quick-search/user.tsx index 6cd04f5858f..b391e6f9e0d 100644 --- a/resources/js/quick-search/user.tsx +++ b/resources/js/quick-search/user.tsx @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. import FlagCountry from 'components/flag-country'; +import FlagTeam from 'components/flag-team'; import FriendButton from 'components/friend-button'; import SupporterIcon from 'components/supporter-icon'; import UserGroupBadges from 'components/user-group-badges'; @@ -26,6 +27,12 @@ export default function User({ user, modifiers = [] }: { modifiers?: string[]; u
+ {user.team != null && +
+ +
+ } + {user.username} 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/home/_search_result_user.blade.php b/resources/views/home/_search_result_user.blade.php index 57c4e093add..efeb19a0731 100644 --- a/resources/views/home/_search_result_user.blade.php +++ b/resources/views/home/_search_result_user.blade.php @@ -2,7 +2,14 @@ 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. --}} +@php + use App\Transformers\UserCompactTransformer; +@endphp
-
+ data-modifiers="{{ json_encode(['search']) }}" + data-users="{{ json_encode(json_collection( + $search->data(), + new UserCompactTransformer(), + UserCompactTransformer::CARD_INCLUDES, + )) }}" +> 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 @@ ">
-
+