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\Exceptions\Handler as ExceptionHandler;
9use App\Jobs\EsDocument;
10use App\Jobs\Notifications\ForumTopicReply;
11use App\Jobs\RegenerateBeatmapsetCover;
12use App\Libraries\Chat;
13use App\Models\Beatmap;
14use App\Models\Beatmapset;
15use App\Models\Chat\Channel;
16use App\Models\Chat\Message;
17use App\Models\Chat\UserChannel;
18use App\Models\Forum;
19use App\Models\NewsPost;
20use App\Models\Notification;
21use App\Models\Score\Best;
22use App\Models\User;
23use App\Models\UserStatistics;
24use App\Transformers\Chat\MessageTransformer;
25use Artisan;
26use Ds\Set;
27use Exception;
28use Illuminate\Foundation\Bus\DispatchesJobs;
29use stdClass;
30
31class LegacyInterOpController extends Controller
32{
33 use DispatchesJobs;
34
35 public function generateNotification()
36 {
37 $params = request()->all();
38
39 if (!isset($params['name'])) {
40 abort(422, 'missing notification name');
41 }
42
43 if ($params['name'] === Notification::FORUM_TOPIC_REPLY) {
44 $post = Forum\Post::find($params['post_id'] ?? null);
45 $user = optional($post)->user;
46
47 if ($post === null || $user === null) {
48 abort(422, 'post is missing or it contains invalid user');
49 }
50
51 (new ForumTopicReply($post, $user))->dispatch();
52
53 return response(null, 204);
54 }
55 }
56
57 public function indexBeatmapset($id)
58 {
59 $beatmapset = Beatmapset::withTrashed()->findOrFail($id);
60
61 if (!$beatmapset->trashed()) {
62 $job = (new RegenerateBeatmapsetCover($beatmapset))->onQueue('beatmap_default');
63 $this->dispatch($job);
64 }
65
66 dispatch(new EsDocument($beatmapset));
67
68 return response(null, 204);
69 }
70
71 public function news()
72 {
73 $newsPosts = NewsPost::default()->limit(5)->get();
74 $posts = [];
75
76 foreach ($newsPosts as $post) {
77 $posts[] = [
78 'timestamp' => $post->published_at->timestamp,
79 'permalink' => route('news.show', $post->slug),
80 'title' => $post->title(),
81 'body' => $post->previewText(),
82 ];
83 }
84
85 return $posts;
86 }
87
88 public function refreshBeatmapsetCache($id)
89 {
90 Beatmapset::findOrFail($id)->refreshCache(true);
91
92 return ['success' => true];
93 }
94
95 /**
96 * User Batch Mark-As-Read (for Chat Channels)
97 *
98 * This endpoint allows you to mark channels as read for users in bulk
99 *
100 * ---
101 *
102 * ### Response Format
103 * empty
104 *
105 * @bodyParam pairs[<id>][user_id] integer required id of user to mark as read for
106 * @bodyParam pairs[<id>][channel_id] integer required id of channel to mark as read
107 */
108 public function userBatchMarkChannelAsRead()
109 {
110 $pairs = request('pairs');
111
112 if (!is_array($pairs)) {
113 abort(422, '"pairs" parameter must be a list');
114 }
115
116 $channelMax = [];
117
118 foreach ($pairs as $pair) {
119 if (!is_array($pair) || !isset($pair['user_id']) || !isset($pair['channel_id'])) {
120 continue;
121 }
122
123 $channelId = get_int($pair['channel_id']);
124 $userId = get_int($pair['user_id']);
125
126 // cache the max message_id of each channel for the duration of this batch
127 $channelMax[$channelId] = $channelMax[$channelId] ??
128 Message::where('channel_id', $channelId)->max('message_id');
129
130 optional(
131 UserChannel::where([
132 'user_id' => $userId,
133 'channel_id' => $channelId,
134 ])->first()
135 )->markAsRead($channelMax[$channelId]);
136 }
137 }
138
139 /**
140 * User Batch Send Message
141 *
142 * This endpoint allows you to send Message as a user to another user.
143 *
144 * ---
145 *
146 * ### Response Format
147 *
148 * Map of <id> and its result.
149 *
150 * Result contains:
151 * - status: status code. 200 if success. See below for list of error codes
152 * - id: id of message being sent
153 * - error: Message of the error (if any)
154 *
155 * Error status codes:
156 * - 403:
157 * - sender not allowed to send message to target
158 * - 404:
159 * - invalid sender/target id
160 * - target is restricted
161 * - 422:
162 * - missing parameter
163 * - message is empty
164 * - message is too long
165 * - target and sender are the same
166 * - 429:
167 * - too many messages has been sent by the sender
168 *
169 * @bodyParam messages[<id>][sender_id] integer required id of user sending the message
170 * @bodyParam messages[<id>][target_id] integer required id of user receiving the message if `type` is `pm`; channel, otherwise. Must not be restricted
171 * @bodyParam messages[<id>][type] string required type of the target of the message. See [ChatChannel](#chatchannel)
172 * @bodyParam messages[<id>][message] string required message to send. Empty string is not allowed
173 * @bodyParam messages[<id>][is_action] boolean required set to true (`1`/`on`/`true`) for `/me` message. Default false
174 */
175 public function userBatchSendMessage()
176 {
177 $params = request('messages');
178
179 $results = new stdClass();
180
181 if (!isset($params)) {
182 return response()->json($results);
183 }
184
185 if (!is_array($params)) {
186 abort(422, '"messages" parameter must be a list');
187 }
188
189 $channelIds = new Set();
190 $userIds = new Set();
191
192 foreach ($params as $key => $messageParams) {
193 if (!is_array($messageParams)) {
194 continue;
195 }
196
197 $messageParams = get_params($messageParams, null, [
198 'sender_id:int',
199 'target_id:int',
200 'type:string',
201 'message:string',
202 'is_action:bool',
203 ]);
204
205 // TODO: default to null later
206 $messageParams['type'] ??= Channel::TYPES['pm'];
207 $messageParams['type'] = strtoupper($messageParams['type']);
208 // TODO: also ignore if type missing (and return error?)
209 if (isset($messageParams['sender_id'])) {
210 $userIds->add($messageParams['sender_id']);
211 }
212
213 if (isset($messageParams['target_id'])) {
214 if ($messageParams['type'] === Channel::TYPES['pm']) {
215 $userIds->add([$messageParams['target_id']]);
216 } else {
217 $channelIds->add([$messageParams['target_id']]);
218 }
219 }
220
221 $params[$key] = $messageParams;
222 }
223
224 $users = User
225 ::whereIn('user_id', $userIds->toArray())
226 ->with(['userGroups', 'blocks'])
227 ->get()
228 ->keyBy('user_id');
229
230 $channels = Channel
231 ::whereIn('channel_id', $channelIds->toArray())
232 ->get()
233 ->keyBy('channel_id');
234
235 foreach ($params as $id => $messageParams) {
236 try {
237 if (!is_array($messageParams)) {
238 abort(422);
239 }
240
241 if (!isset($messageParams['type']) || !isset($messageParams['sender_id']) || !isset($messageParams['target_id'])) {
242 abort(422);
243 }
244
245 $sender = optional($users[$messageParams['sender_id']] ?? null)->markSessionVerified();
246 if ($sender === null) {
247 abort(422, 'sender not found');
248 }
249
250 if ($messageParams['type'] === Channel::TYPES['pm']) {
251 $pmTarget = $users[$messageParams['target_id']] ?? null;
252 if ($pmTarget === null) {
253 abort(422, 'target user not found');
254 }
255
256 $message = Chat::sendPrivateMessage(
257 $sender,
258 $pmTarget,
259 presence($messageParams['message'] ?? null),
260 $messageParams['is_action'] ?? null
261 );
262 } else {
263 $channel = $channels[$messageParams['target_id']] ?? null;
264 if ($channel === null) {
265 abort(422, 'channel not found');
266 }
267
268 $message = Chat::sendMessage(
269 $sender,
270 $channel,
271 presence($messageParams['message'] ?? null),
272 $messageParams['is_action'] ?? false
273 );
274 }
275
276 $result = [
277 'status' => 200,
278 'id' => $message->getKey(),
279 'error' => null,
280 ];
281 } catch (Exception $e) {
282 $result = [
283 'status' => ExceptionHandler::statusCode($e),
284 'id' => null,
285 'error' => ExceptionHandler::exceptionMessage($e),
286 ];
287 }
288
289 datadog_increment('chat.batch', [
290 'status' => $result['status'],
291 ]);
292
293 $results->$id = $result;
294 }
295
296 return response()->json($results);
297 }
298
299 public function userIndex($id)
300 {
301 $user = User::findOrFail($id);
302
303 dispatch(new EsDocument($user));
304
305 foreach (Beatmap::MODES as $modeStr => $modeId) {
306 $class = Best\Model::getClass($modeStr);
307 $class::queueIndexingForUser($user);
308 }
309 Artisan::queue('es:index-scores:queue', [
310 '--all' => true,
311 '--no-interaction' => true,
312 '--user' => $user->getKey(),
313 ]);
314
315 return response(null, 204);
316 }
317
318 public function userRecalculateRankedScores($id)
319 {
320 $user = User::findOrFail($id);
321
322 foreach (Beatmap::MODES as $modeStr => $_modeId) {
323 $class = UserStatistics\Model::getClass($modeStr);
324 $class::recalculateRankedScoreForUser($user);
325 }
326
327 return response(null, 204);
328 }
329
330 /**
331 * User Send Message
332 *
333 * This endpoint allows you to send Message as a user to another user.
334 *
335 * ---
336 *
337 * ### Response Format
338 *
339 * The sent [ChatMessage](#chatmessage) on success.
340 *
341 * - 403 on:
342 * - sender not allowed to send message to target
343 * - 404 on:
344 * - invalid sender/target id
345 * - target is restricted
346 * - 422 on:
347 * - missing parameter
348 * - message is empty
349 * - message is too long
350 * - target and sender are the same
351 * - 429 on:
352 * - too many messages has been sent by the sender
353 *
354 * @bodyParam sender_id integer required id of user sending the message
355 * @bodyParam target_id integer required id of user receiving the message. Must not be restricted
356 * @bodyParam message string required message to send. Empty string is not allowed
357 * @bodyParam is_action boolean required set to true (`1`/`on`/`true`) for `/me` message. Default false
358 */
359 public function userSendMessage()
360 {
361 $params = request()->all();
362
363 $sender = User::findOrFail($params['sender_id'] ?? null)->markSessionVerified();
364 $target = User::lookup($params['target_id'] ?? null, 'id');
365 if ($target === null) {
366 abort(422, 'target user not found');
367 }
368
369 $message = Chat::sendPrivateMessage(
370 $sender,
371 $target,
372 presence($params['message'] ?? null),
373 get_bool($params['is_action'] ?? null)
374 );
375
376 return json_item($message, new MessageTransformer(), ['sender']);
377 }
378
379 public function userSessionsDestroy($userId)
380 {
381 User::find($userId)?->resetSessions();
382
383 return ['success' => true];
384 }
385}