Skip to content

Commit

Permalink
Merge pull request #11810 from nanaya/member-join
Browse files Browse the repository at this point in the history
Add ability to join team
  • Loading branch information
notbakaneko authored Feb 7, 2025
2 parents 6b4a0f6 + 4e66fd9 commit ece7f5b
Show file tree
Hide file tree
Showing 26 changed files with 656 additions and 10 deletions.
78 changes: 78 additions & 0 deletions app/Http/Controllers/Teams/ApplicationsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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\Http\Controllers\Teams;

use App\Http\Controllers\Controller;
use App\Jobs\Notifications\TeamApplicationAccept;
use App\Jobs\Notifications\TeamApplicationReject;
use App\Models\Team;
use App\Models\TeamApplication;
use Symfony\Component\HttpFoundation\Response;

class ApplicationsController extends Controller
{
public function __construct()
{
parent::__construct();

$this->middleware('auth');
}

public function accept(string $teamId, string $id): Response
{
$team = Team::findOrFail($teamId);
$application = $team->applications()->findOrFail($id);

priv_check('TeamApplicationAccept', $application)->ensureCan();

\DB::transaction(function () use ($application, $team) {
$application->delete();
$team->members()->create(['user_id' => $application->getKey()]);
});

(new TeamApplicationAccept($application, \Auth::user()))->dispatch();
\Session::flash('popup', osu_trans('teams.applications.accept.ok'));

return response(null, 204);
}

public function destroy(string $teamId, string $id): Response
{
$currentUser = \Auth::user();
TeamApplication::where('team_id', $teamId)->findOrFail($currentUser->getKey())->delete();

\Session::flash('popup', osu_trans('teams.applications.destroy.ok'));

return response(null, 204);
}

public function reject(string $teamId, string $id): Response
{
$team = Team::findOrFail($teamId);
$application = $team->applications()->findOrFail($id);
priv_check('TeamUpdate', $team)->ensureCan();

$application->delete();

(new TeamApplicationReject($application, \Auth::user()))->dispatch();
\Session::flash('popup', osu_trans('teams.applications.reject.ok'));

return response(null, 204);
}

public function store(string $teamId): Response
{
$team = Team::findOrFail($teamId);
priv_check('TeamApplicationStore', $team)->ensureCan();

$team->applications()->createOrFirst(['user_id' => \Auth::id()]);
\Session::flash('popup', osu_trans('teams.applications.store.ok'));

return response(null, 204);
}
}
10 changes: 10 additions & 0 deletions app/Jobs/Notifications/TeamApplicationAccept.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Jobs\Notifications;

class TeamApplicationAccept extends TeamApplicationBase
{
}
51 changes: 51 additions & 0 deletions app/Jobs/Notifications/TeamApplicationBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Jobs\Notifications;

use App\Models\Notification;
use App\Models\Team;
use App\Models\TeamApplication;
use App\Models\User;

abstract class TeamApplicationBase extends BroadcastNotificationBase
{
const DELIVERY_MODE_DEFAULTS = ['mail' => true, 'push' => true];

protected Team $team;
protected int $userId;

public static function getMailLink(Notification $notification): string
{
return route('teams.show', ['team' => $notification->notifiable_id]);
}

public function __construct(TeamApplication $application, User $source)
{
$this->team = $application->team;
$this->userId = $application->getKey();

parent::__construct($source);
}

public function getDetails(): array
{
return [
'cover_url' => $this->team->logo()->url(),
'team_id' => $this->team->getKey(),
'title' => $this->team->name,
];
}

public function getListeningUserIds(): array
{
return [$this->userId];
}

public function getNotifiable()
{
return $this->team;
}
}
10 changes: 10 additions & 0 deletions app/Jobs/Notifications/TeamApplicationReject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Jobs\Notifications;

class TeamApplicationReject extends TeamApplicationBase
{
}
2 changes: 2 additions & 0 deletions app/Libraries/MorphMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use App\Models\NewsPost;
use App\Models\Score;
use App\Models\Solo;
use App\Models\Team;
use App\Models\User;

class MorphMap
Expand All @@ -44,6 +45,7 @@ class MorphMap
Score\Osu::class => 'score_osu',
Score\Taiko::class => 'score_taiko',
Solo\Score::class => 'solo_score',
Team::class => 'team',
User::class => 'user',
];

Expand Down
6 changes: 6 additions & 0 deletions app/Models/Notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ class Notification extends Model
const CHANNEL_MESSAGE = 'channel_message';
const COMMENT_NEW = 'comment_new';
const FORUM_TOPIC_REPLY = 'forum_topic_reply';
const TEAM_APPLICATION_ACCEPT = 'team_application_accept';
const TEAM_APPLICATION_REJECT = 'team_application_reject';
const USER_ACHIEVEMENT_UNLOCK = 'user_achievement_unlock';
const USER_BEATMAPSET_NEW = 'user_beatmapset_new';
const USER_BEATMAPSET_REVIVE = 'user_beatmapset_revive';

// sync with resources/js/notification-maps/category.ts
const NAME_TO_CATEGORY = [
self::BEATMAP_OWNER_CHANGE => 'beatmap_owner_change',
self::BEATMAPSET_DISCUSSION_LOCK => 'beatmapset_discussion',
Expand All @@ -65,6 +68,8 @@ class Notification extends Model
self::CHANNEL_MESSAGE => 'channel',
self::COMMENT_NEW => 'comment',
self::FORUM_TOPIC_REPLY => 'forum_topic_reply',
self::TEAM_APPLICATION_ACCEPT => 'team_application',
self::TEAM_APPLICATION_REJECT => 'team_application',
self::USER_ACHIEVEMENT_UNLOCK => 'user_achievement_unlock',
self::USER_BEATMAPSET_NEW => 'user_beatmapset_new',
self::USER_BEATMAPSET_REVIVE => 'user_beatmapset_new',
Expand All @@ -74,6 +79,7 @@ class Notification extends Model
Beatmapset::class,
Build::class,
Channel::class,
Team::class,
Topic::class,
NewsPost::class,
User::class,
Expand Down
15 changes: 15 additions & 0 deletions app/Models/Team.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ public function delete()
});
}

public function emptySlots(): int
{
$max = $this->maxMembers();
$current = $this->members->count();

return max(0, $max - $current);
}

public function header(): Uploader
{
return $this->header ??= new Uploader(
Expand Down Expand Up @@ -131,4 +139,11 @@ public function logo(): Uploader
['image' => ['maxDimensions' => [512, 256]]],
);
}

public function maxMembers(): int
{
$this->loadMissing('members.user');

return 8 + (4 * $this->members->filter(fn ($member) => $member->user?->osu_subscriber ?? false)->count());
}
}
27 changes: 27 additions & 0 deletions app/Models/TeamApplication.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;

class TeamApplication extends Model
{
public $incrementing = false;

protected $primaryKey = 'user_id';

public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
7 changes: 7 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
* @property-read Collection<UserDonation> $supporterTagPurchases
* @property-read Collection<UserDonation> $supporterTags
* @property-read Team|null $team
* @property-read TeamApplication|null $teamApplication
* @property-read Collection<OAuth\Token> $tokens
* @property-read Collection<Forum\TopicWatch> $topicWatches
* @property-read Collection<UserAchievement> $userAchievements
Expand Down Expand Up @@ -311,6 +312,11 @@ public function team(): HasOneThrough
);
}

public function teamApplication(): HasOne
{
return $this->hasOne(TeamApplication::class);
}

public function getAuthPassword()
{
return $this->user_password;
Expand Down Expand Up @@ -968,6 +974,7 @@ public function getAttribute($key)
'supporterTagPurchases',
'supporterTags',
'team',
'teamApplication',
'tokens',
'topicWatches',
'userAchievements',
Expand Down
42 changes: 42 additions & 0 deletions app/Singletons/OsuAuthorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use App\Models\Score\Best\Model as ScoreBest;
use App\Models\Solo;
use App\Models\Team;
use App\Models\TeamApplication;
use App\Models\Traits\ReportableInterface;
use App\Models\User;
use App\Models\UserContestEntry;
Expand All @@ -51,6 +52,7 @@ public static function alwaysCheck($ability)
'IsNotOAuth',
'IsOwnClient',
'IsSpecialScope',
'TeamApplicationStore',
'TeamPart',
'UserUpdateEmail',
]);
Expand Down Expand Up @@ -1905,6 +1907,46 @@ public function checkScorePin(?User $user, ScoreBest|Solo\Score $score): string
return 'ok';
}

public function checkTeamApplicationAccept(?User $user, TeamApplication $application): ?string
{
$this->ensureLoggedIn($user);

$team = $application->team;

if ($team->leader_id !== $user->getKey()) {
return null;
}
if ($team->emptySlots() < 1) {
return 'team.application.store.team_full';
}

return 'ok';
}

public function checkTeamApplicationStore(?User $user, Team $team): ?string
{
$prefix = 'team.application.store.';

$this->ensureLoggedIn($user);

if ($user->team !== null) {
return $user->team->getKey() === $team->getKey()
? $prefix.'already_member'
: $prefix.'already_other_member';
}
if ($user->teamApplication()->exists()) {
return $prefix.'currently_applying';
}
if (!$team->is_open) {
return $prefix.'team_closed';
}
if ($team->emptySlots() < 1) {
return $prefix.'team_full';
}

return 'ok';
}

public function checkTeamPart(?User $user, Team $team): ?string
{
$this->ensureLoggedIn($user);
Expand Down
25 changes: 25 additions & 0 deletions database/factories/TeamMemberFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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\Team;
use App\Models\TeamMember;
use App\Models\User;

class TeamMemberFactory extends Factory
{
protected $model = TeamMember::class;

public function definition(): array
{
return [
'team_id' => Team::factory(),
'user_id' => User::factory(),
];
}
}
Loading

0 comments on commit ece7f5b

Please sign in to comment.