the browser-facing portion of osu!
at master 12 kB view raw
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}