1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Http\Controllers;
7
8use App\Jobs\UpdateUserFollowerCountCache;
9use App\Models\User;
10use App\Models\UserRelation;
11use App\Transformers\UserCompactTransformer;
12use App\Transformers\UserRelationTransformer;
13
14class FriendsController extends Controller
15{
16 public function __construct()
17 {
18 $this->middleware('auth');
19
20 $this->middleware('verify-user', [
21 'only' => [
22 'store',
23 'destroy',
24 ],
25 ]);
26
27 $this->middleware('require-scopes:friends.read', ['only' => ['index']]);
28
29 parent::__construct();
30 }
31
32 public function index()
33 {
34 $currentUser = \Auth::user();
35 $currentMode = default_mode();
36
37 $relationFriends = $currentUser->relationFriends->sortBy('username');
38 $relationFriends->load(array_map(
39 fn ($userPreload) => "target.{$userPreload}",
40 UserCompactTransformer::listIncludesPreload($currentMode),
41 ));
42
43 $isApi = is_api_request();
44
45 if ($isApi && api_version() >= 20241022) {
46 return json_collection($relationFriends, new UserRelationTransformer(), [
47 "target:ruleset({$currentMode})",
48 ...array_map(
49 fn ($userInclude) => "target.{$userInclude}",
50 UserCompactTransformer::LIST_INCLUDES,
51 ),
52 ]);
53 }
54
55 $friends = $relationFriends->pluck('target');
56 $usersJson = json_collection(
57 $friends,
58 (new UserCompactTransformer())->setMode($currentMode),
59 UserCompactTransformer::LIST_INCLUDES
60 );
61
62 return $isApi
63 ? $usersJson
64 : ext_view('friends.index', compact('usersJson'));
65 }
66
67 public function store()
68 {
69 $currentUser = \Auth::user();
70
71 if ($currentUser->friends()->count() >= $currentUser->maxFriends()) {
72 return error_popup(osu_trans('friends.too_many'));
73 }
74
75 $targetId = get_int(request('target'));
76 $targetUser = User::lookup($targetId, 'id');
77
78 if ($targetUser === null) {
79 abort(404);
80 }
81
82 if ($currentUser->getKey() === $targetId) {
83 abort(422);
84 }
85
86 $relationQuery = $currentUser->relations()->where('zebra_id', $targetId);
87 while (true) {
88 $existingRelation = $relationQuery->first();
89 $updateCount = false;
90
91 if ($existingRelation === null) {
92 try {
93 UserRelation::create([
94 'user_id' => $currentUser->getKey(),
95 'zebra_id' => $targetId,
96 'friend' => true,
97 ]);
98 $updateCount = true;
99 } catch (\Throwable $e) {
100 if (is_sql_unique_exception($e)) {
101 // redo the loop with what should be a non-null
102 // $existingRelation on the next one
103 continue;
104 }
105
106 throw $e;
107 }
108 } elseif (!$existingRelation->friend) {
109 $existingRelation->update([
110 'friend' => true,
111 'foe' => false,
112 ]);
113 $updateCount = true;
114 }
115
116 break;
117 }
118
119 if ($updateCount) {
120 dispatch(new UpdateUserFollowerCountCache($targetId));
121 }
122
123 return [
124 'user_relation' => json_item(
125 $relationQuery->withMutual()->first(),
126 new UserRelationTransformer(),
127 ),
128 ];
129 }
130
131 public function destroy($id)
132 {
133 $currentUser = \Auth::user();
134
135 $currentUser
136 ->friends()
137 ->wherePivot('zebra_id', $id)
138 ->firstOrFail();
139
140 UserRelation::where([
141 'user_id' => $currentUser->getKey(),
142 'zebra_id' => $id,
143 'friend' => 1,
144 ])->delete();
145
146 dispatch(new UpdateUserFollowerCountCache($id));
147
148 return response(null, 204);
149 }
150}