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}