the browser-facing portion of osu!
at master 9.7 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\Libraries\UserChannelList; 10use App\Models\Chat\Message; 11use App\Models\User; 12use App\Models\UserAccountHistory; 13use App\Transformers\Chat\ChannelTransformer; 14use App\Transformers\Chat\MessageTransformer; 15use App\Transformers\Chat\UserSilenceTransformer; 16use Ds\Set; 17 18/** 19 * @group Chat 20 */ 21class ChatController extends Controller 22{ 23 public function __construct() 24 { 25 $this->middleware('require-scopes:chat.read', ['only' => ['ack']]); 26 $this->middleware('require-scopes:chat.write', ['only' => ['newConversation']]); 27 28 // TODO: remove as it's already defined in parent controller? 29 $this->middleware('auth'); 30 31 parent::__construct(); 32 } 33 34 /** 35 * Chat Keepalive 36 * 37 * Request periodically to reset chat activity timeout. Also returns an updated list of recent silences. 38 * 39 * See [Public channels and activity timeout](#public-channels-and-activity-timeout) 40 * 41 * --- 42 * 43 * ### Response Format 44 * 45 * Field | Type 46 * ---------------- | ----------------- 47 * silences | [UserSilence](#usersilence)[] 48 * 49 * @queryParam history_since integer [UserSilence](#usersilence)s after the specified id to return. 50 * This field is preferred and takes precedence over `since`. 51 * @queryParam since integer [UserSilence](#usersilence)s after the specified [ChatMessage.message_id](#chatmessage) to return. No-example 52 */ 53 public function ack() 54 { 55 Chat::ack(auth()->user()); 56 57 $params = get_params(request()->all(), null, [ 58 'history_since:int', 59 'since:int', 60 ], ['null_missing' => true]); 61 62 return [ 63 'silences' => json_collection($this->getSilences($params['history_since'], $params['since'] ?? 0), new UserSilenceTransformer()), 64 ]; 65 } 66 67 /** 68 * Create New PM 69 * 70 * This endpoint allows you to create a new PM channel. 71 * 72 * --- 73 * 74 * ### Response Format 75 * 76 * Field | Type 77 * ---------------- | ----------------- 78 * channel | The new [ChatChannel](#chatchannel) 79 * message | the sent [ChatMessage](#chatmessage) 80 * new_channel_id | Deprecated; `channel_id` of newly created [ChatChannel](#chatchannel) 81 * 82 * <aside class="notice"> 83 * This endpoint will only allow the creation of PMs initially, group chat support will come later. 84 * </aside> 85 * 86 * @bodyParam target_id integer required `user_id` of user to start PM with 87 * @bodyParam message string required message to send 88 * @bodyParam is_action boolean required whether the message is an action 89 * @bodyParam uuid string client-side message identifier which will be sent back in response and websocket json. Example: some-uuid-string 90 * 91 * @response { 92 * "channel": { 93 * "channel_id": 1234, 94 * "description": "", 95 * "icon": "https://a.ppy.sh/102?1500537068" 96 * "message_length_limit": 450, 97 * "moderated": false, 98 * "name": "peppy", 99 * "type": "PM", 100 * "uuid": null, 101 * "last_message_id": 9150005005, 102 * "users": [ 103 * 101, 104 * 102 105 * ] 106 * }, 107 * "message": { 108 * "channel_id": 1234, 109 * "content": "i can haz featured artist plz?", 110 * "is_action": false, 111 * "message_id": 9150005005, 112 * "sender_id": 102, 113 * "timestamp": "2024-12-23T01:23:45+00:00", 114 * "type": "plain", 115 * "uuid": "some-uuid-string", 116 * "sender": { 117 * "avatar_url": "https://a.ppy.sh/102?1500537068", 118 * "country_code": "AU", 119 * "default_group": "default", 120 * "id": 102, 121 * "is_active": true, 122 * "is_bot": false, 123 * "is_deleted": false, 124 * "is_online": true, 125 * "is_supporter": true 126 * "last_visit": "2024-12-23T01:23:45+00:00", 127 * "pm_friends_only": false, 128 * "profile_colour": "#333333", 129 * "username": "nekodex", 130 * } 131 * }, 132 * "new_channel_id": 1234, 133 * } 134 */ 135 public function newConversation() 136 { 137 $params = get_params(request()->all(), null, [ 138 'is_action:bool', 139 'message', 140 'target_id:int', 141 'uuid', 142 ], ['null_missing' => true]); 143 144 $target = User::lookup($params['target_id'], 'id'); 145 if ($target === null) { 146 abort(422, 'target user not found'); 147 } 148 149 $sender = auth()->user(); 150 151 /** @var Message $message */ 152 $message = Chat::sendPrivateMessage( 153 $sender, 154 $target, 155 $params['message'], 156 $params['is_action'], 157 $params['uuid'] 158 ); 159 160 $channelJson = json_item($message->channel, ChannelTransformer::forUser($sender), ChannelTransformer::CONVERSATION_INCLUDES); 161 162 return [ 163 'channel' => $channelJson, 164 'message' => json_item( 165 $message, 166 new MessageTransformer(), 167 ['sender'] 168 ), 169 'new_channel_id' => $message->channel_id, // TODO: remove, there's channel already. 170 ]; 171 } 172 173 // TODO: move the listing to channels.index 174 /** 175 * @deprecated 176 * @group Undocumented 177 */ 178 public function presence() 179 { 180 return (new UserChannelList(auth()->user()))->get(); 181 } 182 183 /** 184 * Get Updates 185 * 186 * Returns the list of channels the current User is in along with an updated list of [UserSilence](#usersilence)s. 187 * 188 * --- 189 * 190 * ### Response Format 191 * 192 * Field | Type 193 * ---------------- | ----------------- 194 * messages | This field is not used and will be removed. 195 * presence | [ChatChannel](#chatchannel)[]? 196 * silences | [UserSilence](#usersilence)[]? 197 * 198 * @queryParam history_since integer [UserSilence](#usersilence)s after the specified id to return. 199 * This field is preferred and takes precedence over `since`. 200 * @queryParam includes string[] List of fields from `presence`, `silences` to include in the response. Returns all if not specified. No-example 201 * @queryParam since integer [UserSilence](#usersilence)s after the specified [ChatMessage.message_id](#chatmessage) to return. No-example 202 * 203 * @response { 204 * "presence": [ 205 * { 206 * "channel_id": 5, 207 * "current_user_attributes": { 208 * "can_message": true, 209 * "can_message_error": null, 210 * "last_read_id": 9150005005 211 * }, 212 * "name": "#osu", 213 * "description": "The official osu! channel (english only).", 214 * "type": "public", 215 * "last_read_id": 9150005005, 216 * "last_message_id": 9150005005 217 * }, 218 * { 219 * "channel_id": 12345, 220 * "current_user_attributes": { 221 * "can_message": true, 222 * "can_message_error": null, 223 * "last_read_id": 9150001235 224 * }, 225 * "type": "PM", 226 * "name": "peppy", 227 * "icon": "https://a.ppy.sh/2?1519081077.png", 228 * "users": [ 229 * 2, 230 * 102 231 * ], 232 * "last_read_id": 9150001235, 233 * "last_message_id": 9150001234 234 * } 235 * ], 236 * "silences": [ 237 * { 238 * "id": 1, 239 * "user_id": 2 240 * } 241 * ] 242 * } 243 */ 244 public function updates() 245 { 246 static $availableIncludes; 247 $availableIncludes ??= new Set(['messages', 'presence', 'silences']); 248 249 $params = get_params(request()->all(), null, [ 250 'history_since:int', 251 'includes:array', 252 'since:int', 253 ], ['null_missing' => true]); 254 255 if ($params['since'] === null) { 256 abort(422); 257 } 258 259 $includes = $params['includes'] !== null 260 ? $availableIncludes->intersect(new Set($params['includes'])) 261 : $availableIncludes; 262 263 $response = []; 264 265 if ($includes->contains('presence')) { 266 $userChannelList = new UserChannelList(auth()->user()); 267 $response['presence'] = $userChannelList->get(); 268 } 269 270 if ($includes->contains('silences')) { 271 $silences = $this->getSilences($params['history_since'], $params['since']); 272 $response['silences'] = json_collection($silences, new UserSilenceTransformer()); 273 } 274 275 // FIXME: empty array for compatibility with old lazer versions 276 if ($includes->contains('messages')) { 277 $response['messages'] = []; 278 } 279 280 return $response; 281 } 282 283 private function getSilences(?int $lastHistoryId, ?int $since) 284 { 285 $silenceQuery = UserAccountHistory::bans()->recentForChat()->limit(100); 286 287 if ($lastHistoryId === null) { 288 $previousMessage = Message::where('message_id', '<=', $since)->last(); 289 290 if ($previousMessage === null) { 291 $silenceQuery->none(); 292 } else { 293 $silenceQuery->where('timestamp', '>', $previousMessage->timestamp); 294 } 295 } else { 296 $silenceQuery->where('ban_id', '>', $lastHistoryId)->reorderBy('ban_id', 'DESC'); 297 } 298 299 return $silenceQuery->get(); 300 } 301}