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;
7
8use App\Exceptions\ModelNotSavedException;
9use App\Libraries\BeatmapsetDiscussion\Discussion;
10use App\Libraries\BeatmapsetDiscussion\Reply;
11use App\Libraries\BeatmapsetDiscussion\Review;
12use App\Libraries\BeatmapsetDiscussionPostsBundle;
13use App\Models\BeatmapDiscussion;
14use App\Models\BeatmapDiscussionPost;
15use App\Models\Beatmapset;
16use App\Models\BeatmapsetWatch;
17use App\Models\User;
18
19/**
20 * @group Beatmapset Discussions
21 */
22class BeatmapDiscussionPostsController extends Controller
23{
24 public function __construct()
25 {
26 $this->middleware('auth', ['except' => ['index', 'show']]);
27 $this->middleware('require-scopes:public', ['only' => ['index']]);
28
29 parent::__construct();
30 }
31
32 public function destroy($id)
33 {
34 $post = BeatmapDiscussionPost::whereNull('deleted_at')->findOrFail($id);
35 priv_check('BeatmapDiscussionPostDestroy', $post)->ensureCan();
36
37 try {
38 $post->softDeleteOrExplode(auth()->user());
39 } catch (ModelNotSavedException $e) {
40 return error_popup($e->getMessage());
41 }
42
43 return $post->beatmapset->defaultDiscussionJson();
44 }
45
46 /**
47 * Get Beatmapset Discussion Posts
48 *
49 * Returns the posts of beatmapset discussions.
50 *
51 * ---
52 *
53 * ### Response Format
54 *
55 * <aside class="warning">
56 * The response of this endpoint is likely to change soon!
57 * </aside>
58 *
59 * Field | Type | Description
60 * ------------- | ------------------------------------------------------- | -----------
61 * beatmapsets | [Beatmapset](#beatmapset) | |
62 * cursor_string | [CursorString](#cursorstring) | |
63 * posts | [BeatmapsetDiscussionPost](#beatmapsetdiscussionpost)[] | |
64 * users | [User](#user) | |
65 *
66 * @queryParam beatmapset_discussion_id `id` of the [BeatmapsetDiscussion](#beatmapsetdiscussion).
67 * @queryParam limit Maximum number of results.
68 * @queryParam page Search result page.
69 * @queryParam sort `id_desc` for newest first; `id_asc` for oldest first. Defaults to `id_desc`.
70 * @queryParam types[] `first`, `reply`, `system` are the valid values. Defaults to `reply`.
71 * @queryParam user The `id` of the [User](#user).
72 * @queryParam with_deleted This param has no effect as api calls do not currently receive group permissions.
73 */
74 public function index()
75 {
76 $bundle = new BeatmapsetDiscussionPostsBundle(request()->all());
77
78 if (is_api_request()) {
79 return $bundle->toArray();
80 }
81
82 $posts = $bundle->getPaginator();
83
84 return ext_view('beatmap_discussion_posts.index', compact('posts'));
85 }
86
87 public function restore($id)
88 {
89 $post = BeatmapDiscussionPost::whereNotNull('deleted_at')->findOrFail($id);
90 priv_check('BeatmapDiscussionPostRestore', $post)->ensureCan();
91
92 $post->restore(auth()->user());
93
94 return $post->beatmapset->defaultDiscussionJson();
95 }
96
97 public function show($id)
98 {
99 $post = BeatmapDiscussionPost::findOrFail($id);
100 $discussion = $post->beatmapDiscussion;
101 $beatmapset = $discussion->beatmapset;
102
103 if ($beatmapset === null) {
104 abort(404);
105 }
106
107 return ujs_redirect(route('beatmapsets.discussion', $beatmapset).'#/'.$discussion->getKey().'/'.$post->getKey());
108 }
109
110 public function store()
111 {
112 /** @var User $user */
113 $user = auth()->user();
114 $request = request()->all();
115
116 $discussionId = get_int($request['beatmap_discussion_id'] ?? null);
117 $beatmapsetId = get_int($request['beatmapset_id'] ?? null);
118
119 $message = presence(get_string($request['beatmap_discussion_post']['message'] ?? null));
120
121 if ($discussionId !== null) {
122 $discussion = BeatmapDiscussion::findOrFail($discussionId);
123 $resolve = get_bool($request['beatmap_discussion']['resolved'] ?? null);
124 $posts = (new Reply($user, $discussion, $message, $resolve))->handle();
125
126 $beatmapset = $discussion->beatmapset;
127 } elseif ($beatmapsetId !== null) {
128 $beatmapset = Beatmapset::findOrFail($beatmapsetId);
129 $discussionParams = get_params($request, 'beatmap_discussion', [
130 'beatmap_id:int',
131 'message_type',
132 'timestamp:int',
133 ], ['null_missing' => true]);
134
135 [$discussion, $posts] = (new Discussion($user, $beatmapset, $discussionParams, $message))->handle();
136 } else {
137 abort(404);
138 }
139
140 BeatmapsetWatch::markRead($beatmapset, $user);
141
142 return [
143 'beatmapset' => $beatmapset->defaultDiscussionJson(),
144 'beatmap_discussion_post_ids' => array_pluck($posts, 'id'),
145 'beatmap_discussion_id' => $discussion->getKey(),
146 ];
147 }
148
149 public function update($id)
150 {
151 $post = BeatmapDiscussionPost::findOrFail($id);
152
153 priv_check('BeatmapDiscussionPostEdit', $post)->ensureCan();
154
155 $params = get_params(request()->all(), 'beatmap_discussion_post', ['message']);
156 $params['last_editor_id'] = auth()->user()->user_id;
157
158 if ($post->beatmapDiscussion->message_type === 'review' && $post->isFirstPost()) {
159 // handle reviews (but not replies to the reviews)
160 try {
161 $document = json_decode($params['message'], true);
162 Review::update($post->beatmapDiscussion, $document, auth()->user());
163 } catch (\Exception $e) {
164 throw new ModelNotSavedException($e->getMessage());
165 }
166 } else {
167 $post->fill($params)->saveOrExplode();
168 }
169
170 return $post->beatmapset->defaultDiscussionJson();
171 }
172}