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\Models\LegacyMatch\LegacyMatch;
9use App\Models\User;
10use App\Transformers\LegacyMatch\EventTransformer;
11use App\Transformers\UserCompactTransformer;
12
13/**
14 * @group Matches
15 */
16class MatchesController extends Controller
17{
18 public function __construct()
19 {
20 $this->middleware('require-scopes:public', ['only' => ['index', 'show']]);
21 }
22
23 /**
24 * Get Matches Listing
25 *
26 * Returns a list of matches.
27 *
28 * ---
29 *
30 * ### Response Format
31 *
32 * Field | Type | Notes
33 * ------------- | ----------------------------- | -----
34 * cursor | [Cursor](#cursor) | |
35 * cursor_string | [CursorString](#cursorstring) | |
36 * matches | [Match](#match)[] | |
37 * params.limit | integer | |
38 * params.sort | string | |
39 *
40 * @usesCursor
41 * @queryParam limit integer Maximum number of matches (50 default, 1 minimum, 50 maximum). No-example
42 * @queryParam sort string `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`. No-example
43 * @response {
44 * "matches": [
45 * {
46 * "id": 114428685,
47 * "start_time": "2024-06-25T00:55:30+00:00",
48 * "end_time": null,
49 * "name": "peppy's game"
50 * },
51 * // ...
52 * ],
53 * "params": {
54 * "limit": 50,
55 * "sort": "id_desc"
56 * },
57 * "cursor": {
58 * "match_id": 114428685
59 * },
60 * "cursor_string": "eyJtYXRjaF9pZCI6MTE0NDI4Njg1fQ"
61 * }
62 */
63 public function index()
64 {
65 $params = request()->all();
66 $limit = \Number::clamp(get_int($params['limit'] ?? null) ?? 50, 1, 50);
67 $cursorHelper = LegacyMatch::makeDbCursorHelper($params['sort'] ?? null);
68
69 [$matches, $hasMore] = LegacyMatch
70 ::where('private', false)
71 ->cursorSort($cursorHelper, cursor_from_params($params))
72 ->limit($limit)
73 ->getWithHasMore();
74
75 return [
76 'matches' => json_collection($matches, 'LegacyMatch\LegacyMatch'),
77 'params' => ['limit' => $limit, 'sort' => $cursorHelper->getSortName()],
78 ...cursor_for_response($cursorHelper->next($matches, $hasMore)),
79 ];
80 }
81
82 /**
83 * Get Match
84 *
85 * Returns details of the specified match.
86 *
87 * ---
88 *
89 * ### Response Format
90 *
91 * Field | Type | Notes
92 * --------------- | --------------------------- | -----
93 * match | [Match](#match) | |
94 * events | [MatchEvent](#matchevent)[] | |
95 * users | [User](#user)[] | Includes `country`.
96 * first_event_id | integer | ID of the first [MatchEvent](#matchevent) in the match.
97 * latest_event_id | integer | ID of the lastest [MatchEvent](#matchevent) in the match.
98 *
99 * @urlParam match integer required Match ID. No-example
100 * @queryParam before integer Filter for match events before the specified [MatchEvent.id](#matchevent). No-example
101 * @queryParam after integer Filter for match events after the specified [MatchEvent.id](#matchevent). No-example
102 * @queryParam limit integer Maximum number of match events (100 default, 1 minimum, 101 maximum). No-example
103 * @response {
104 * "match": {
105 * "id": 16155689,
106 * "start_time": "2015-05-16T09:44:51+00:00",
107 * "end_time": "2015-05-16T10:55:08+00:00",
108 * "name": "CWC 2015: (Australia) vs (Poland)"
109 * },
110 * "events": [
111 * {
112 * "id": 484385927,
113 * "detail": {
114 * "type": "match-created"
115 * },
116 * "timestamp": "2015-05-16T09:44:51+00:00",
117 * "user_id": null
118 * },
119 * // ...
120 * ],
121 * "users": [],
122 * "first_event_id": 484385927,
123 * "latest_event_id": 484410607,
124 * "current_game_id": null
125 * }
126 */
127 public function show($id)
128 {
129 $match = LegacyMatch::findOrFail($id);
130
131 $params = get_params(request()->all(), null, ['after:int', 'before:int', 'limit:int']);
132 $params['match'] = $match;
133
134 priv_check('MatchView', $match)->ensureCan();
135
136 $eventsJson = $this->eventsJson($params);
137
138 if (is_json_request()) {
139 return $eventsJson;
140 } else {
141 return ext_view('matches.index', compact('match', 'eventsJson'));
142 }
143 }
144
145 private function eventsJson($params)
146 {
147 $match = $params['match'];
148 $after = $params['after'] ?? null;
149 $before = $params['before'] ?? null;
150 $limit = \Number::clamp($params['limit'] ?? 100, 1, 101);
151
152 $events = $match->events()
153 ->with([
154 'game.beatmap.beatmapset',
155 'game.scores' => fn ($q) => $q->default(),
156 ])->limit($limit);
157
158 if (isset($after)) {
159 $events
160 ->where('event_id', '>', $after)
161 ->orderBy('event_id', 'ASC');
162 } else {
163 if (isset($before)) {
164 $events->where('event_id', '<', $before);
165 }
166
167 $events->orderBy('event_id', 'DESC');
168 $reverseOrder = true;
169 }
170
171 $events = $events->get();
172 foreach ($events as $event) {
173 $game = $event->game;
174 if ($game !== null) {
175 foreach ($game->scores as $score) {
176 $score->setRelation('game', $game);
177 }
178 }
179 }
180
181 if ($reverseOrder ?? false) {
182 $events = $events->reverse();
183 }
184
185 $users = User::with('country')->whereIn('user_id', $this->usersFromEvents($events))->get();
186
187 $users = json_collection(
188 $users,
189 new UserCompactTransformer(),
190 'country'
191 );
192
193 $events = json_collection(
194 $events,
195 new EventTransformer(),
196 ['game.beatmap.beatmapset', 'game.scores.match']
197 );
198
199 $eventEndIds = $match
200 ->events()
201 ->selectRaw('MIN(event_id) first_event_id, MAX(event_id) latest_event_id')
202 ->first();
203
204 return [
205 'match' => json_item($match, 'LegacyMatch\LegacyMatch'),
206 'events' => $events,
207 'users' => $users,
208 'first_event_id' => $eventEndIds->first_event_id ?? 0,
209 'latest_event_id' => $eventEndIds->latest_event_id ?? 0,
210 'current_game_id' => optional($match->currentGame())->getKey(),
211 ];
212 }
213
214 private function usersFromEvents($events)
215 {
216 $userIds = [];
217
218 foreach ($events as $event) {
219 if ($event->user_id) {
220 $userIds[] = $event->user_id;
221 }
222
223 if ($event->game) {
224 foreach ($event->game->scores as $score) {
225 $userIds[] = $score->user_id;
226 }
227 }
228 }
229
230 return array_unique($userIds);
231 }
232}