Skip to content

Commit 551a11a

Browse files
authoredFeb 19, 2025
Merge pull request #11835 from nanaya/team-create
Add team creation page
2 parents 56c33a8 + 0af9a66 commit 551a11a

File tree

18 files changed

+442
-32
lines changed

18 files changed

+442
-32
lines changed
 

Diff for: ‎.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ CLIENT_CHECK_VERSION=false
249249

250250
# OAUTH_MAX_USER_CLIENTS=1
251251

252+
# TEAM_CREATE_REQUIRE_SUPPORTER=false
253+
# TEAM_MAX_MEMBERS=40
254+
252255
# USER_REPORT_NOTIFICATION_ENDPOINT_CHEATING=
253256
# default if nothing specified for specific type
254257
# USER_REPORT_NOTIFICATION_ENDPOINT_MODERATION=

Diff for: ‎app/Http/Controllers/TeamsController.php

+41-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace App\Http\Controllers;
99

1010
use App\Exceptions\InvariantException;
11+
use App\Exceptions\ModelNotSavedException;
1112
use App\Models\Beatmap;
1213
use App\Models\Team;
1314
use App\Models\User;
@@ -19,7 +20,7 @@ class TeamsController extends Controller
1920
public function __construct()
2021
{
2122
parent::__construct();
22-
$this->middleware('auth', ['only' => ['part']]);
23+
$this->middleware('auth', ['only' => ['create', 'part']]);
2324
}
2425

2526
public static function pageLinks(string $current, Team $team): array
@@ -55,6 +56,19 @@ public static function pageLinks(string $current, Team $team): array
5556
return $ret;
5657
}
5758

59+
public function create(): Response
60+
{
61+
$currentUser = \Auth::user();
62+
$teamId = $currentUser?->team?->getKey() ?? $currentUser?->teamApplication?->team_id;
63+
if ($teamId !== null) {
64+
return ujs_redirect(route('teams.show', $teamId));
65+
}
66+
67+
return ext_view('teams.create', [
68+
'team' => new Team(),
69+
]);
70+
}
71+
5872
public function destroy(string $id): Response
5973
{
6074
$team = Team::findOrFail($id);
@@ -118,6 +132,31 @@ public function show(string $id): Response
118132
return ext_view('teams.show', compact('team'));
119133
}
120134

135+
public function store(): Response
136+
{
137+
priv_check('TeamStore')->ensureCan();
138+
139+
$params = get_params(\Request::all(), 'team', [
140+
'name',
141+
'short_name',
142+
]);
143+
144+
$user = \Auth::user();
145+
$team = (new Team([...$params, 'leader_id' => $user->getKey()]));
146+
try {
147+
\DB::transaction(function () use ($team, $user) {
148+
$team->saveOrExplode();
149+
$team->members()->create(['user_id' => $user->getKey()]);
150+
});
151+
} catch (ModelNotSavedException) {
152+
return ext_view('teams.create', compact('team'), status: 422);
153+
}
154+
155+
\Session::flash('popup', osu_trans('teams.store.ok'));
156+
157+
return ujs_redirect(route('teams.show', $team));
158+
}
159+
121160
public function update(string $id): Response
122161
{
123162
$team = Team::findOrFail($id);
@@ -135,7 +174,7 @@ public function update(string $id): Response
135174

136175
$team->fill($params)->saveOrExplode();
137176

138-
\Session::flash('popup', osu_trans('teams.edit.saved'));
177+
\Session::flash('popup', osu_trans('teams.edit.ok'));
139178

140179
return response(null, 201);
141180
}

Diff for: ‎app/Libraries/UsernameValidation.php

+13-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717

1818
class UsernameValidation
1919
{
20+
public static function allowedName(string $username): bool
21+
{
22+
foreach (model_pluck(DB::table('phpbb_disallow'), 'disallow_username') as $check) {
23+
if (preg_match('#^'.str_replace('%', '.*?', preg_quote($check, '#')).'$#i', $username)) {
24+
return false;
25+
}
26+
}
27+
28+
return true;
29+
}
30+
2031
public static function validateAvailability(string $username): ValidationErrors
2132
{
2233
$errors = new ValidationErrors('user');
@@ -72,11 +83,8 @@ public static function validateUsername($username)
7283
$errors->add('username', '.username_no_space_userscore_mix');
7384
}
7485

75-
foreach (model_pluck(DB::table('phpbb_disallow'), 'disallow_username') as $check) {
76-
if (preg_match('#^'.str_replace('%', '.*?', preg_quote($check, '#')).'$#i', $username)) {
77-
$errors->add('username', '.username_not_allowed');
78-
break;
79-
}
86+
if (!static::allowedName($username)) {
87+
$errors->add('username', '.username_not_allowed');
8088
}
8189

8290
return $errors;

Diff for: ‎app/Models/Team.php

+58-3
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,27 @@
99

1010
use App\Libraries\BBCodeForDB;
1111
use App\Libraries\Uploader;
12+
use App\Libraries\UsernameValidation;
1213
use Illuminate\Database\Eloquent\Relations\BelongsTo;
1314
use Illuminate\Database\Eloquent\Relations\HasMany;
1415

1516
class Team extends Model
1617
{
18+
const MAX_FIELD_LENGTHS = [
19+
'name' => 100,
20+
'short_name' => 4,
21+
];
22+
1723
protected $casts = ['is_open' => 'bool'];
1824

1925
private Uploader $header;
2026
private Uploader $logo;
2127

28+
private static function sanitiseName(?string $value): ?string
29+
{
30+
return presence(preg_replace('/ +/', ' ', trim($value ?? '')));
31+
}
32+
2233
public function applications(): HasMany
2334
{
2435
return $this->hasMany(TeamApplication::class);
@@ -34,6 +45,11 @@ public function members(): HasMany
3445
return $this->hasMany(TeamMember::class);
3546
}
3647

48+
public function setDefaultRulesetIdAttribute(?int $value): void
49+
{
50+
$this->attributes['default_ruleset_id'] = Beatmap::MODES[Beatmap::modeStr($value) ?? 'osu'];
51+
}
52+
3753
public function setHeaderAttribute(?string $value): void
3854
{
3955
if ($value === null) {
@@ -52,9 +68,14 @@ public function setLogoAttribute(?string $value): void
5268
}
5369
}
5470

55-
public function setDefaultRulesetIdAttribute(?int $value): void
71+
public function setNameAttribute(?string $value): void
5672
{
57-
$this->attributes['default_ruleset_id'] = Beatmap::MODES[Beatmap::modeStr($value) ?? 'osu'];
73+
$this->attributes['name'] = static::sanitiseName($value);
74+
}
75+
76+
public function setShortNameAttribute(?string $value): void
77+
{
78+
$this->attributes['short_name'] = static::sanitiseName($value);
5879
}
5980

6081
public function setUrlAttribute(?string $value): void
@@ -114,6 +135,24 @@ public function isValid(): bool
114135
{
115136
$this->validationErrors()->reset();
116137

138+
$wordFilters = app('chat-filters');
139+
foreach (['name', 'short_name'] as $field) {
140+
$value = $this->$field;
141+
if ($value === null) {
142+
$this->validationErrors()->add($field, 'required');
143+
} elseif ($this->isDirty($field)) {
144+
if (!preg_match('#^[A-Za-z0-9-\[\]_ ]+$#u', $value)) {
145+
$this->validationErrors()->add($field, '.invalid_characters');
146+
} elseif (!$wordFilters->isClean($value) || !UsernameValidation::allowedName($value)) {
147+
$this->validationErrors()->add($field, '.word_not_allowed');
148+
} elseif (static::whereNot('id', $this->getKey())->where($field, $value)->exists()) {
149+
$this->validationErrors()->add($field, '.used');
150+
}
151+
}
152+
}
153+
154+
$this->validateDbFieldLengths();
155+
117156
if ($this->isDirty('url')) {
118157
$url = $this->url;
119158
if ($url !== null && !is_http($url)) {
@@ -144,6 +183,22 @@ public function maxMembers(): int
144183
{
145184
$this->loadMissing('members.user');
146185

147-
return 8 + (4 * $this->members->filter(fn ($member) => $member->user?->osu_subscriber ?? false)->count());
186+
$supporterCount = $this->members->filter(fn ($member) => $member->user?->isSupporter() ?? false)->count();
187+
188+
return min(8 + (4 * $supporterCount), $GLOBALS['cfg']['osu']['team']['max_members']);
189+
}
190+
191+
public function save(array $options = [])
192+
{
193+
if (!$this->isValid()) {
194+
return false;
195+
}
196+
197+
return parent::save($options);
198+
}
199+
200+
public function validationErrorsTranslationPrefix(): string
201+
{
202+
return 'team';
148203
}
149204
}

Diff for: ‎app/Singletons/ChatFilters.php

+45-16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ private static function combinedFilterRegex($filters): string
2929
return "/{$regex}/iu";
3030
}
3131

32+
public function isClean(string $text): bool
33+
{
34+
$filters = $this->filterRegexps();
35+
36+
foreach ($filters['non_whitespace_delimited_replaces'] as $search => $_replacement) {
37+
if (stripos($text, $search) !== false) {
38+
return false;
39+
}
40+
}
41+
42+
$patterns = [
43+
$filters['block_regex'] ?? null,
44+
...array_keys($filters['whitespace_delimited_replaces']),
45+
];
46+
47+
foreach ($patterns as $pattern) {
48+
if ($pattern !== null && preg_match($pattern, $text)) {
49+
return false;
50+
}
51+
}
52+
53+
return true;
54+
}
55+
3256
/**
3357
* Applies all active chat filters to the provided text.
3458
* @param string $text The text to filter.
@@ -38,7 +62,27 @@ private static function combinedFilterRegex($filters): string
3862
*/
3963
public function filter(string $text): string
4064
{
41-
$filters = $this->memoize(__FUNCTION__, function () {
65+
$filters = $this->filterRegexps();
66+
67+
if (isset($filters['block_regex']) && preg_match($filters['block_regex'], $text)) {
68+
throw new ContentModerationException();
69+
}
70+
71+
$text = str_ireplace(
72+
array_keys($filters['non_whitespace_delimited_replaces']),
73+
array_values($filters['non_whitespace_delimited_replaces']),
74+
$text
75+
);
76+
return preg_replace(
77+
array_keys($filters['whitespace_delimited_replaces']),
78+
array_values($filters['whitespace_delimited_replaces']),
79+
$text
80+
);
81+
}
82+
83+
private function filterRegexps(): array
84+
{
85+
return $this->memoize(__FUNCTION__, function () {
4286
$ret = [];
4387

4488
$allFilters = ChatFilter::all();
@@ -63,20 +107,5 @@ public function filter(string $text): string
63107

64108
return $ret;
65109
});
66-
67-
if (isset($filters['block_regex']) && preg_match($filters['block_regex'], $text)) {
68-
throw new ContentModerationException();
69-
}
70-
71-
$text = str_ireplace(
72-
array_keys($filters['non_whitespace_delimited_replaces']),
73-
array_values($filters['non_whitespace_delimited_replaces']),
74-
$text
75-
);
76-
return preg_replace(
77-
array_keys($filters['whitespace_delimited_replaces']),
78-
array_values($filters['whitespace_delimited_replaces']),
79-
$text
80-
);
81110
}
82111
}

Diff for: ‎app/Singletons/OsuAuthorize.php

+23
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public static function alwaysCheck($ability)
5555
'TeamApplicationAccept',
5656
'TeamApplicationStore',
5757
'TeamPart',
58+
'TeamStore',
59+
'TeamUpdate',
5860
'UserUpdateEmail',
5961
]);
6062

@@ -1966,6 +1968,27 @@ public function checkTeamPart(?User $user, Team $team): ?string
19661968
return 'ok';
19671969
}
19681970

1971+
public function checkTeamStore(?User $user): ?string
1972+
{
1973+
$this->ensureLoggedIn($user);
1974+
$this->ensureCleanRecord($user);
1975+
$this->ensureHasPlayed($user);
1976+
1977+
if ($GLOBALS['cfg']['osu']['team']['create_require_supporter'] && !$user->isSupporter()) {
1978+
return 'team.store.require_supporter_tag';
1979+
}
1980+
1981+
if ($user->team !== null) {
1982+
return 'team.application.store.already_other_member';
1983+
}
1984+
1985+
if ($user->teamApplication !== null) {
1986+
return 'team.application.store.currently_applying';
1987+
}
1988+
1989+
return 'ok';
1990+
}
1991+
19691992
public function checkTeamUpdate(?User $user, Team $team): ?string
19701993
{
19711994
$this->ensureLoggedIn($user);

Diff for: ‎config/osu.php

+4
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@
207207
'tags_cache_duration' => 60 * (get_int(env('TAGS_CACHE_DURATION')) ?? 60), // in minutes, converted to seconds
208208
'beatmap_tags_cache_duration' => 60 * (get_int(env('BEATMAP_TAGS_CACHE_DURATION')) ?? 60), // in minutes, converted to seconds
209209
],
210+
'team' => [
211+
'create_require_supporter' => get_bool(env('TEAM_CREATE_REQUIRE_SUPPORTER')) ?? false,
212+
'max_members' => get_int(env('TEAM_MAX_MEMBERS')) ?? 40,
213+
],
210214
'twitch_client_id' => presence(env('TWITCH_CLIENT_ID')),
211215
'twitch_client_secret' => presence(env('TWITCH_CLIENT_SECRET')),
212216
'urls' => [

Diff for: ‎database/factories/TeamFactory.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ public function configure(): static
2424
public function definition(): array
2525
{
2626
return [
27-
'name' => fn () => $this->faker->name(),
28-
'short_name' => fn () => $this->faker->domainWord(),
27+
'name' => fn () => strtr($this->faker->unique()->userName(), '.', ' '),
28+
'short_name' => fn () => substr(strtr($this->faker->unique()->userName(), '.', ' '), 0, 4),
2929
'leader_id' => User::factory(),
3030
];
3131
}

0 commit comments

Comments
 (0)