the browser-facing portion of osu!
at master 10 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\Chat; 7 8use App\Libraries\Chat; 9use App\Models\Chat\Channel; 10use App\Models\Chat\UserChannel; 11use App\Models\User; 12use App\Transformers\Chat\ChannelTransformer; 13use App\Transformers\UserCompactTransformer; 14use Auth; 15 16/** 17 * @group Chat 18 */ 19class ChannelsController extends Controller 20{ 21 public function __construct() 22 { 23 $this->middleware('require-scopes:chat.read', ['only' => ['index', 'markAsRead', 'show']]); 24 $this->middleware('require-scopes:chat.write_manage', ['only' => ['part', 'join', 'store']]); 25 26 parent::__construct(); 27 } 28 29 /** 30 * Get Channel List 31 * 32 * This endpoint returns a list of all joinable public channels. 33 * 34 * --- 35 * 36 * ### Response Format 37 * 38 * Returns an array of [ChatChannel](#chatchannel) 39 * 40 * @response [ 41 * { 42 * "channel_id": 5, 43 * "description": "The official osu! channel (english only).", 44 * "icon": "https://a.ppy.sh/2?1519081077.png", 45 * "moderated": false, 46 * "name": "#osu", 47 * "type": "public" 48 * } 49 * ] 50 */ 51 public function index() 52 { 53 return json_collection( 54 Channel::public()->get(), 55 ChannelTransformer::forUser(auth()->user()) 56 ); 57 } 58 59 /** 60 * Join Channel 61 * 62 * This endpoint allows you to join a public or multiplayer channel. 63 * 64 * --- 65 * 66 * ### Response Format 67 * 68 * Returns the joined [ChatChannel](#chatchannel). 69 * 70 * @response { 71 * "channel_id": 5, 72 * "current_user_attributes": { 73 * "can_message": true, 74 * "can_message_error": null 75 * }, 76 * "description": "The official osu! channel (english only).", 77 * "icon": "https://a.ppy.sh/2?1519081077.png", 78 * "last_message_id": 1029, 79 * "moderated": false, 80 * "name": "#osu", 81 * "type": "public" 82 * "users": [] 83 * } 84 */ 85 public function join($channelId, $userId) 86 { 87 $channel = Channel::where('channel_id', $channelId)->firstOrFail(); 88 $currentUser = auth()->user(); 89 90 priv_check('ChatChannelJoin', $channel)->ensureCan(); 91 92 if ($currentUser->getKey() !== get_int($userId)) { 93 abort(403); 94 } 95 96 $channel->addUser($currentUser); 97 98 return json_item($channel, ChannelTransformer::forUser($currentUser), ChannelTransformer::LISTING_INCLUDES); 99 } 100 101 /** 102 * Leave Channel 103 * 104 * This endpoint allows you to leave a public channel. 105 * 106 * --- 107 * 108 * ### Response Format 109 * 110 * _empty response_ 111 * 112 * <aside class="notice"> 113 * This endpoint will only allow the leaving of public channels initially. 114 * </aside> 115 * 116 * @response 204 117 */ 118 public function part($channelId, $userId) 119 { 120 $channel = Channel::where('channel_id', $channelId)->firstOrFail(); 121 122 // TODO: the order of these check seems wrong? 123 // FIXME: doesn't seem right authorizing leaving channel 124 priv_check('ChatChannelPart', $channel)->ensureCan(); 125 126 if (Auth::user()->user_id !== get_int($userId)) { 127 abort(403); 128 } 129 130 $channel->removeUser(Auth::user()); 131 132 return response([], 204); 133 } 134 135 /** 136 * Get Channel 137 * 138 * Gets details of a chat channel. 139 * 140 * --- 141 * 142 * ### Response Format 143 * 144 * Field | Type | Description 145 * ------- | --------------------------- | ----------- 146 * channel | [ChatChannel](#chatchannel) | | 147 * users | [User](#user) | Users are only visible for PM channels. 148 * 149 * @response { 150 * "channel": { 151 * "channel_id": 1337, 152 * "current_user_attributes": { 153 * "can_message": true, 154 * "can_message_error": null 155 * }, 156 * "name": "test channel", 157 * "description": "wheeeee", 158 * "icon": "/images/layout/avatar-guest@2x.png", 159 * "type": "PM", 160 * "last_message_id": 9150005005, 161 * "moderated": false, 162 * "users": [ 163 * 2, 164 * 102 165 * ] 166 * }, 167 * "users": [ 168 * { 169 * "id": 2, 170 * "username": "peppy", 171 * "profile_colour": "#3366FF", 172 * "avatar_url": "https://a.ppy.sh/2?1519081077.png", 173 * "country_code": "AU", 174 * "is_active": true, 175 * "is_bot": false, 176 * "is_deleted": false, 177 * "is_online": true, 178 * "is_supporter": true 179 * }, 180 * { 181 * "id": 102, 182 * "username": "lambchop", 183 * "profile_colour": "#3366FF", 184 * "icon": "/images/layout/avatar-guest@2x.png", 185 * "country_code": "NZ", 186 * "is_active": true, 187 * "is_bot": false, 188 * "is_deleted": false, 189 * "is_online": false, 190 * "is_supporter": false 191 * } 192 * ] 193 * } 194 */ 195 public function show($channelId) 196 { 197 $channel = Channel::where('channel_id', $channelId)->firstOrFail(); 198 $user = auth()->user(); 199 200 if (!$channel->hasUser($user)) { 201 abort(404); 202 } 203 204 return [ 205 'channel' => json_item($channel, ChannelTransformer::forUser($user), ChannelTransformer::LISTING_INCLUDES), 206 // TODO: probably going to need a better way to list/fetch/update users on larger channels without sending user on every message. 207 'users' => json_collection( 208 $channel->visibleUsers()->loadMissing(UserCompactTransformer::CARD_INCLUDES_PRELOAD), 209 new UserCompactTransformer(), 210 UserCompactTransformer::CARD_INCLUDES 211 ), 212 ]; 213 } 214 215 /** 216 * Create Channel 217 * 218 * Creates a new PM or announcement channel. 219 * Rejoins the PM channel if it already exists. 220 * 221 * --- 222 * 223 * ### Response Format 224 * 225 * Returns [ChatChannel](#chatchannel) with `recent_messages` attribute; `recent_messages` is deprecated and should not be used. 226 * 227 * @bodyParam channel object channel details; required if `type` is `ANNOUNCE`. No-example 228 * @bodyParam channel.name string the channel name; required if `type` is `ANNOUNCE`. No-example 229 * @bodyParam channel.description string the channel description; required if `type` is `ANNOUNCE`. No-example 230 * @bodyParam message string message to send with the announcement; required if `type` is `ANNOUNCE`. No-example 231 * @bodyParam target_id integer target user id; required if `type` is `PM`; ignored, otherwise. Example: 2 232 * @bodyParam target_ids integer[] target user ids; required if `type` is `ANNOUNCE`; ignored, otherwise. No-example 233 * @bodyParam type string required channel type (currently only supports `PM` and `ANNOUNCE`) Example: PM 234 * 235 * @response { 236 * "channel_id": 1, 237 * "description": "best channel", 238 * "icon": "https://a.ppy.sh/2?1519081077.png", 239 * "moderated": false, 240 * "name": "#pm_1-2", 241 * "type": "PM", 242 * "recent_messages": [ 243 * { 244 * "message_id": 1, 245 * "sender_id": 1, 246 * "channel_id": 1, 247 * "timestamp": "2020-01-01T00:00:00+00:00", 248 * "content": "Happy new year", 249 * "is_action": false, 250 * "sender": { 251 * "id": 2, 252 * "username": "peppy", 253 * "profile_colour": "#3366FF", 254 * "avatar_url": "https://a.ppy.sh/2?1519081077.png", 255 * "country_code": "AU", 256 * "is_active": true, 257 * "is_bot": false, 258 * "is_online": true, 259 * "is_supporter": true 260 * } 261 * } 262 * ] 263 * } 264 */ 265 public function store() 266 { 267 $params = get_params(request()->all(), null, [ 268 'channel:any', 269 'message:string', 270 'target_id:int', 271 'target_ids:int[]', 272 'type:string', 273 'uuid', 274 ], ['null_missing' => true]); 275 276 $sender = auth()->user(); 277 278 if ($params['type'] === Channel::TYPES['pm']) { 279 abort_if($params['target_id'] === null, 422, 'missing target_id parameter'); 280 281 $target = User::findOrFail($params['target_id']); 282 283 priv_check('ChatPmStart', $target)->ensureCan(); 284 285 $channel = Channel::findPM($sender, $target) ?? new Channel(); 286 287 if ($channel->exists) { 288 $channel->addUser($sender); 289 } 290 } else if ($params['type'] === Channel::TYPES['announce']) { 291 $channel = Chat::createAnnouncement($sender, $params); 292 } 293 294 if (isset($channel)) { 295 // TODO: recent_messages deprecated. 296 return json_item($channel, ChannelTransformer::forUser($sender), ['recent_messages.sender']); 297 } else { 298 abort(422, 'unknown or missing type parameter'); 299 } 300 } 301 302 /** 303 * Mark Channel as Read 304 * 305 * This endpoint marks the channel as having being read up to the given `message_id`. 306 * 307 * --- 308 * 309 * ### Response Format 310 * 311 * _empty response_ 312 * 313 * <aside class="notice"> 314 * Note that the read marker cannot be moved backwards - i.e. if a channel has been marked as read up to <code>message_id = 12</code>, you cannot then set it backwards to <code>message_id = 10</code>. It will be rejected. 315 * </aside> 316 * 317 * @queryParam channel_id required The `channel_id` of the channel to mark as read 318 * @queryParam message_id required The `message_id` of the message to mark as read up to 319 * 320 * @response 204 321 */ 322 public function markAsRead($channelId, $messageId) 323 { 324 UserChannel::where([ 325 'user_id' => Auth::user()->user_id, 326 'channel_id' => $channelId, 327 ]) 328 ->firstOrFail() 329 ->markAsRead(get_int($messageId)); 330 331 return response([], 204); 332 } 333}