. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.
namespace App\Http\Controllers\Chat;
use App\Libraries\Chat;
use App\Models\Chat\Channel;
use App\Models\Chat\UserChannel;
use App\Models\User;
use App\Transformers\Chat\ChannelTransformer;
use App\Transformers\UserCompactTransformer;
use Auth;
/**
* @group Chat
*/
class ChannelsController extends Controller
{
public function __construct()
{
$this->middleware('require-scopes:chat.read', ['only' => ['index', 'markAsRead', 'show']]);
$this->middleware('require-scopes:chat.write_manage', ['only' => ['part', 'join', 'store']]);
parent::__construct();
}
/**
* Get Channel List
*
* This endpoint returns a list of all joinable public channels.
*
* ---
*
* ### Response Format
*
* Returns an array of [ChatChannel](#chatchannel)
*
* @response [
* {
* "channel_id": 5,
* "description": "The official osu! channel (english only).",
* "icon": "https://a.ppy.sh/2?1519081077.png",
* "moderated": false,
* "name": "#osu",
* "type": "public"
* }
* ]
*/
public function index()
{
return json_collection(
Channel::public()->get(),
ChannelTransformer::forUser(auth()->user())
);
}
/**
* Join Channel
*
* This endpoint allows you to join a public or multiplayer channel.
*
* ---
*
* ### Response Format
*
* Returns the joined [ChatChannel](#chatchannel).
*
* @response {
* "channel_id": 5,
* "current_user_attributes": {
* "can_message": true,
* "can_message_error": null
* },
* "description": "The official osu! channel (english only).",
* "icon": "https://a.ppy.sh/2?1519081077.png",
* "last_message_id": 1029,
* "moderated": false,
* "name": "#osu",
* "type": "public"
* "users": []
* }
*/
public function join($channelId, $userId)
{
$channel = Channel::where('channel_id', $channelId)->firstOrFail();
$currentUser = auth()->user();
priv_check('ChatChannelJoin', $channel)->ensureCan();
if ($currentUser->getKey() !== get_int($userId)) {
abort(403);
}
$channel->addUser($currentUser);
return json_item($channel, ChannelTransformer::forUser($currentUser), ChannelTransformer::LISTING_INCLUDES);
}
/**
* Leave Channel
*
* This endpoint allows you to leave a public channel.
*
* ---
*
* ### Response Format
*
* _empty response_
*
*
*
* @response 204
*/
public function part($channelId, $userId)
{
$channel = Channel::where('channel_id', $channelId)->firstOrFail();
// TODO: the order of these check seems wrong?
// FIXME: doesn't seem right authorizing leaving channel
priv_check('ChatChannelPart', $channel)->ensureCan();
if (Auth::user()->user_id !== get_int($userId)) {
abort(403);
}
$channel->removeUser(Auth::user());
return response([], 204);
}
/**
* Get Channel
*
* Gets details of a chat channel.
*
* ---
*
* ### Response Format
*
* Field | Type | Description
* ------- | --------------------------- | -----------
* channel | [ChatChannel](#chatchannel) | |
* users | [User](#user) | Users are only visible for PM channels.
*
* @response {
* "channel": {
* "channel_id": 1337,
* "current_user_attributes": {
* "can_message": true,
* "can_message_error": null
* },
* "name": "test channel",
* "description": "wheeeee",
* "icon": "/images/layout/avatar-guest@2x.png",
* "type": "PM",
* "last_message_id": 9150005005,
* "moderated": false,
* "users": [
* 2,
* 102
* ]
* },
* "users": [
* {
* "id": 2,
* "username": "peppy",
* "profile_colour": "#3366FF",
* "avatar_url": "https://a.ppy.sh/2?1519081077.png",
* "country_code": "AU",
* "is_active": true,
* "is_bot": false,
* "is_deleted": false,
* "is_online": true,
* "is_supporter": true
* },
* {
* "id": 102,
* "username": "lambchop",
* "profile_colour": "#3366FF",
* "icon": "/images/layout/avatar-guest@2x.png",
* "country_code": "NZ",
* "is_active": true,
* "is_bot": false,
* "is_deleted": false,
* "is_online": false,
* "is_supporter": false
* }
* ]
* }
*/
public function show($channelId)
{
$channel = Channel::where('channel_id', $channelId)->firstOrFail();
$user = auth()->user();
if (!$channel->hasUser($user)) {
abort(404);
}
return [
'channel' => json_item($channel, ChannelTransformer::forUser($user), ChannelTransformer::LISTING_INCLUDES),
// TODO: probably going to need a better way to list/fetch/update users on larger channels without sending user on every message.
'users' => json_collection(
$channel->visibleUsers()->loadMissing(UserCompactTransformer::CARD_INCLUDES_PRELOAD),
new UserCompactTransformer(),
UserCompactTransformer::CARD_INCLUDES
),
];
}
/**
* Create Channel
*
* Creates a new PM or announcement channel.
* Rejoins the PM channel if it already exists.
*
* ---
*
* ### Response Format
*
* Returns [ChatChannel](#chatchannel) with `recent_messages` attribute; `recent_messages` is deprecated and should not be used.
*
* @bodyParam channel object channel details; required if `type` is `ANNOUNCE`. No-example
* @bodyParam channel.name string the channel name; required if `type` is `ANNOUNCE`. No-example
* @bodyParam channel.description string the channel description; required if `type` is `ANNOUNCE`. No-example
* @bodyParam message string message to send with the announcement; required if `type` is `ANNOUNCE`. No-example
* @bodyParam target_id integer target user id; required if `type` is `PM`; ignored, otherwise. Example: 2
* @bodyParam target_ids integer[] target user ids; required if `type` is `ANNOUNCE`; ignored, otherwise. No-example
* @bodyParam type string required channel type (currently only supports `PM` and `ANNOUNCE`) Example: PM
*
* @response {
* "channel_id": 1,
* "description": "best channel",
* "icon": "https://a.ppy.sh/2?1519081077.png",
* "moderated": false,
* "name": "#pm_1-2",
* "type": "PM",
* "recent_messages": [
* {
* "message_id": 1,
* "sender_id": 1,
* "channel_id": 1,
* "timestamp": "2020-01-01T00:00:00+00:00",
* "content": "Happy new year",
* "is_action": false,
* "sender": {
* "id": 2,
* "username": "peppy",
* "profile_colour": "#3366FF",
* "avatar_url": "https://a.ppy.sh/2?1519081077.png",
* "country_code": "AU",
* "is_active": true,
* "is_bot": false,
* "is_online": true,
* "is_supporter": true
* }
* }
* ]
* }
*/
public function store()
{
$params = get_params(request()->all(), null, [
'channel:any',
'message:string',
'target_id:int',
'target_ids:int[]',
'type:string',
'uuid',
], ['null_missing' => true]);
$sender = auth()->user();
if ($params['type'] === Channel::TYPES['pm']) {
abort_if($params['target_id'] === null, 422, 'missing target_id parameter');
$target = User::findOrFail($params['target_id']);
priv_check('ChatPmStart', $target)->ensureCan();
$channel = Channel::findPM($sender, $target) ?? new Channel();
if ($channel->exists) {
$channel->addUser($sender);
}
} else if ($params['type'] === Channel::TYPES['announce']) {
$channel = Chat::createAnnouncement($sender, $params);
}
if (isset($channel)) {
// TODO: recent_messages deprecated.
return json_item($channel, ChannelTransformer::forUser($sender), ['recent_messages.sender']);
} else {
abort(422, 'unknown or missing type parameter');
}
}
/**
* Mark Channel as Read
*
* This endpoint marks the channel as having being read up to the given `message_id`.
*
* ---
*
* ### Response Format
*
* _empty response_
*
*
*
* @queryParam channel_id required The `channel_id` of the channel to mark as read
* @queryParam message_id required The `message_id` of the message to mark as read up to
*
* @response 204
*/
public function markAsRead($channelId, $messageId)
{
UserChannel::where([
'user_id' => Auth::user()->user_id,
'channel_id' => $channelId,
])
->firstOrFail()
->markAsRead(get_int($messageId));
return response([], 204);
}
}