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\Channels;
7
8use App\Http\Controllers\Chat\Controller as BaseController;
9use App\Libraries\Chat;
10use App\Models\Chat\Channel;
11use App\Models\Chat\Message;
12use App\Transformers\Chat\MessageTransformer;
13use App\Transformers\UserCompactTransformer;
14
15/**
16 * @group Chat
17 */
18class MessagesController extends BaseController
19{
20 public function __construct()
21 {
22 $this->middleware('require-scopes:chat.read', ['only' => ['index']]);
23 $this->middleware('require-scopes:chat.write', ['only' => ['store']]);
24
25 parent::__construct();
26 }
27
28 /**
29 * Get Channel Messages
30 *
31 * This endpoint returns the chat messages for a specific channel.
32 *
33 * ---
34 *
35 * ### Response Format
36 *
37 * Returns an array of [ChatMessage](#chatmessage)
38 *
39 * @urlParam channel integer required The ID of the channel to retrieve messages for
40 * @queryParam limit integer number of messages to return (max of 50)
41 * @queryParam since integer messages after the specified message id will be returned
42 * @queryParam until integer messages up to but not including the specified message id will be returned
43 *
44 * @response [
45 * {
46 * "message_id": 9150005004,
47 * "sender_id": 2,
48 * "channel_id": 5,
49 * "timestamp": "2018-07-06T06:33:34+00:00",
50 * "content": "i am a lazerface",
51 * "is_action": 0,
52 * "sender": {
53 * "id": 2,
54 * "username": "peppy",
55 * "profile_colour": "#3366FF",
56 * "avatar_url": "https://a.ppy.sh/2?1519081077.png",
57 * "country_code": "AU",
58 * "is_active": true,
59 * "is_bot": false,
60 * "is_online": true,
61 * "is_supporter": true
62 * }
63 * },
64 * {
65 * "message_id": 9150005005,
66 * "sender_id": 102,
67 * "channel_id": 5,
68 * "timestamp": "2018-07-06T06:33:42+00:00",
69 * "content": "uh ok then",
70 * "is_action": 0,
71 * "sender": {
72 * "id": 102,
73 * "username": "nekodex",
74 * "profile_colour": "#333333",
75 * "avatar_url": "https://a.ppy.sh/102?1500537068",
76 * "country_code": "AU",
77 * "is_active": true,
78 * "is_bot": false,
79 * "is_online": true,
80 * "is_supporter": true
81 * }
82 * }
83 * ]
84 */
85 public function index($channelId)
86 {
87 [
88 'limit' => $limit,
89 'return_object' => $returnObject,
90 'since' => $since,
91 'until' => $until,
92 ] = get_params(request()->all(), null, [
93 'limit:int',
94 'return_object:bool',
95 'since:int',
96 'until:int',
97 ], ['null_missing' => true]);
98
99 $limit = \Number::clamp($limit ?? 50, 1, 50);
100 $user = auth()->user();
101
102 $channel = Channel::findOrFail($channelId);
103 if (!$channel->hasUser($user)) {
104 abort(404);
105 }
106
107 if ($channel->isPM()) {
108 // restricted users should be treated as if they do not exist
109 if (optional($channel->pmTargetFor($user))->isRestricted()) {
110 abort(404);
111 }
112 }
113
114 $messages = $channel
115 ->messages()
116 ->with(['channel', 'sender'])
117 ->limit($limit);
118
119 if (present($since)) {
120 $messages = $messages->where('message_id', '>', $since)
121 ->orderBy('message_id', 'asc')
122 ->get();
123 } else {
124 if (present($until)) {
125 $messages->where('message_id', '<', $until);
126 }
127
128 $messages = $messages->orderBy('message_id', 'desc')->get()->reverse();
129 }
130
131 $messages = Message::filterBacklogs($channel, $messages);
132
133 if (!$returnObject) {
134 return json_collection(
135 $messages,
136 new MessageTransformer(),
137 ['sender']
138 );
139 }
140
141 return [
142 'messages' => json_collection($messages, new MessageTransformer()),
143 // FIXME: messages with null used should be removed from db...
144 'users' => json_collection(
145 $messages->pluck('sender')->filter()->uniqueStrict('user_id')->values(),
146 new UserCompactTransformer()
147 ),
148 ];
149 }
150
151 /**
152 * Send Message to Channel
153 *
154 * This endpoint returns the chat messages for a specific channel.
155 *
156 * ---
157 *
158 * ### Response Format
159 *
160 * The sent [ChatMessage](#chatmessage)
161 *
162 * <aside class="notice">
163 * When sending a message, the <code>last_read_id</code> for the <a href='#chatchannel'>ChatChannel</a> is also updated to mark the new message as read.
164 * </aside>
165 *
166 * @urlParam channel integer required The `channel_id` of the channel to send message to
167 *
168 * @bodyParam message string required message to send
169 * @bodyParam is_action boolean required whether the message is an action
170 *
171 * @response {
172 * "message_id": 9150005004,
173 * "sender_id": 2,
174 * "channel_id": 5,
175 * "timestamp": "2018-07-06T06:33:34+00:00",
176 * "content": "i am a lazerface",
177 * "is_action": 0,
178 * "sender": {
179 * "id": 2,
180 * "username": "peppy",
181 * "profile_colour": "#3366FF",
182 * "avatar_url": "https://a.ppy.sh/2?1519081077.png",
183 * "country_code": "AU",
184 * "is_active": true,
185 * "is_bot": false,
186 * "is_online": true,
187 * "is_supporter": true
188 * }
189 * }
190 */
191 public function store($channelId)
192 {
193 $params = get_params(request()->all(), null, [
194 'is_action:bool',
195 'message',
196 'uuid',
197 ], ['null_missing' => true]);
198
199 $message = Chat::sendMessage(
200 auth()->user(),
201 Channel::findOrFail(get_int($channelId)),
202 $params['message'],
203 $params['is_action'] ?? false,
204 $params['uuid']
205 );
206
207 return json_item(
208 $message,
209 new MessageTransformer(),
210 ['sender']
211 );
212 }
213}