Skip to content

Commit

Permalink
Merge pull request ppy#11742 from Sheppsu/forum-api
Browse files Browse the repository at this point in the history
Add new API endpoints for forums and topics
  • Loading branch information
nanaya authored Feb 13, 2025
2 parents 029b1d0 + 8c29900 commit cb80e1b
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 12 deletions.
93 changes: 83 additions & 10 deletions app/Http/Controllers/Forum/ForumsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,43 @@
use App\Models\Forum\Topic;
use App\Models\Forum\TopicTrack;
use App\Transformers\Forum\ForumCoverTransformer;
use App\Transformers\Forum\ForumTransformer;
use App\Transformers\Forum\TopicTransformer;
use Auth;

/**
* @group Forum
*/
class ForumsController extends Controller
{
public function __construct()
{
parent::__construct();

$this->middleware('require-scopes:public', ['only' => ['index', 'show']]);
}

/**
* Get Forum Listing
*
* Get top-level forums, their subforums (max 2 deep), and their last topics.
*
* ---
*
* ### Response Format
*
* Field | Type |
* ----------- | ---------------------------- |
* forums | [Forum](#forum)[] |
* last_topics | [ForumTopic](#forum-topic)[] |
*
* @response {
* "forums": [
* { "forum_id": 1, "...": "..." },
* { "forum_id": 2, "...": "..." }
* ]
* }
*/
public function index()
{
$forums = Forum
Expand All @@ -27,12 +55,18 @@ public function index()
->orderBy('left_id')
->get();

$lastTopics = Forum::lastTopics();

$forums = $forums->filter(function ($forum) {
return priv_check('ForumView', $forum)->can();
});

if (is_api_request()) {
return [
'forums' => json_collection($forums, new ForumTransformer(), ['subforums.subforums']),
];
}

$lastTopics = Forum::lastTopics();

return ext_view('forum.forums.index', compact('forums', 'lastTopics'));
}

Expand All @@ -53,6 +87,35 @@ public function markAsRead()
return ext_view('layout.ujs-reload', [], 'js');
}

/**
* Get Forum and Topics
*
* Get a forum by id, its pinned topics, recent topics, and its subforums.
*
* ---
*
* ### Response Format
*
* Field | Type |
* ------------- | ---------------------------- |
* forum | [Forum](#forum) |
* topics | [ForumTopic](#forum-topic)[] |
* pinned_topics | [ForumTopic](#forum-topic)[] |
*
* @urlParam forum integer required Id of the forum. Example: 1
*
* @response {
* "forum": { "id": 1, "...": "..." },
* "topics": [
* { "id": 1, "...": "..." },
* { "id": 2, "...": "..." },
* ],
* "pinned_topics": [
* { "id": 1, "...": "..." },
* { "id": 2, "...": "..." },
* ]
* }
*/
public function show($id)
{
$params = get_params(request()->all(), null, [
Expand All @@ -63,18 +126,12 @@ public function show($id)
$user = auth()->user();

$forum = Forum::with('subforums.subforums')->findOrFail($id);
$lastTopics = Forum::lastTopics($forum);

$sort = $params['sort'] ?? Topic::DEFAULT_SORT;
$withReplies = $params['with_replies'] ?? null;

priv_check('ForumView', $forum)->ensureCan();

$cover = json_item(
$forum->cover()->firstOrNew([]),
new ForumCoverTransformer()
);

$showDeleted = priv_check('ForumModerate', $forum)->can();

$pinnedTopics = $forum->topics()
Expand All @@ -88,8 +145,19 @@ public function show($id)
->with('forum')
->normal()
->showDeleted($showDeleted)
->recent(compact('sort', 'withReplies'))
->paginate();
->recent(compact('sort', 'withReplies'));

if (is_api_request()) {
return [
'forum' => json_item($forum, new ForumTransformer(), ['subforums.subforums']),
'topics' => json_collection($topics->limit(Topic::PER_PAGE)->get(), new TopicTransformer()),
'pinned_topics' => json_collection($pinnedTopics, new TopicTransformer()),
];
}

$topics = $topics->paginate();

$lastTopics = Forum::lastTopics($forum);

$allTopics = array_merge($pinnedTopics->all(), $topics->all());
$topicReadStatus = TopicTrack::readStatus($user, $allTopics);
Expand All @@ -103,6 +171,11 @@ public function show($id)
->get()
->keyBy('topic_id');

$cover = json_item(
$forum->cover()->firstOrNew([]),
new ForumCoverTransformer()
);

$noindex = !$forum->enable_indexing;

set_opengraph($forum);
Expand Down
57 changes: 56 additions & 1 deletion app/Http/Controllers/Forum/TopicsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use App\Models\Forum\TopicWatch;
use App\Models\UserProfileCustomization;
use App\Transformers\Forum\TopicCoverTransformer;
use App\Transformers\Forum\TopicTransformer;
use Auth;
use DB;
use Request;
Expand Down Expand Up @@ -50,7 +51,7 @@ public function __construct()
'store',
]]);

$this->middleware('require-scopes:public', ['only' => ['show']]);
$this->middleware('require-scopes:public', ['only' => ['index', 'show']]);
$this->middleware('require-scopes:forum.write', ['only' => ['reply', 'store', 'update']]);
}

Expand Down Expand Up @@ -279,6 +280,60 @@ public function reply($id)
}
}

/**
* Get Topic Listing
*
* Get a sorted list of topics, optionally from a specific forum
*
* ---
*
* ### Response Format
*
* Field | Type | Notes
* ------------- | ----------------------------- | -----
* topics | [ForumTopic](#forum-topic)[] | |
* cursor_string | [CursorString](#cursorstring) | |
*
* @usesCursor
* @queryParam forum_id Id of a specific forum to get topics from. No-example
* @queryParam sort Topic sorting option. Valid values are `new` (default) and `old`. Both sort by the topic's last post time. No-example
* @queryParam limit Maximum number of topics to be returned (50 at most and by default). No-example
*
* @response {
* "topics": [
* { "id": 1, "...": "..." },
* { "id": 2, "...": "..." }
* ],
* "cursor_string": "eyJoZWxsbyI6IndvcmxkIn0"
* }
*/
public function index()
{
$params = get_params(request()->all(), null, [
'limit:int',
'sort',
'forum_id:int',
], ['null_missing' => true]);

$limit = \Number::clamp($params['limit'] ?? Topic::PER_PAGE, 1, Topic::PER_PAGE);
$cursorHelper = Topic::makeDbCursorHelper($params['sort']);

$topics = Topic::cursorSort($cursorHelper, cursor_from_params($params))
->limit($limit);

$forum_id = $params['forum_id'];
if ($forum_id !== null) {
$topics->where('forum_id', $forum_id);
}

[$topics, $hasMore] = $topics->getWithHasMore();

return [
'topics' => json_collection($topics, new TopicTransformer()),
...cursor_for_response($cursorHelper->next($topics, $hasMore)),
];
}

/**
* Get Topic and Posts
*
Expand Down
11 changes: 11 additions & 0 deletions app/Models/Forum/Topic.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use App\Models\Beatmapset;
use App\Models\Log;
use App\Models\Notification;
use App\Models\Traits\WithDbCursorHelper;
use App\Models\User;
use App\Traits\Memoizes;
use App\Traits\Validatable;
Expand Down Expand Up @@ -79,8 +80,18 @@ class Topic extends Model implements AfterCommit
use SoftDeletes {
restore as private origRestore;
}
use WithDbCursorHelper;

const DEFAULT_SORT = 'new';
const SORTS = [
'new' => [
// type 'timestamp' because the values are stored as integer in the database
['column' => 'topic_last_post_time', 'order' => 'DESC', 'type' => 'timestamp'],
],
'old' => [
['column' => 'topic_last_post_time', 'order' => 'ASC', 'type' => 'timestamp'],
],
];

const STATUS_LOCKED = 1;
const STATUS_UNLOCKED = 0;
Expand Down
34 changes: 34 additions & 0 deletions app/Transformers/Forum/ForumTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

namespace App\Transformers\Forum;

use App\Models\Forum\Forum;
use App\Transformers\TransformerAbstract;
use League\Fractal\Resource\ResourceInterface;

class ForumTransformer extends TransformerAbstract
{
protected array $availableIncludes = [
'subforums',
];

public function transform(Forum $forum): array
{
return [
'id' => $forum->getKey(),
'name' => $forum->forum_name,
'description' => $forum->forum_desc,
];
}

public function includeSubforums(Forum $forum): ResourceInterface
{
return $this->collection(
$forum->subforums,
new static()
);
}
}
7 changes: 7 additions & 0 deletions app/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,8 @@ function get_param_value($input, $type)
return get_arr($input, 'get_int');
case 'time':
return parse_time_to_carbon($input);
case 'timestamp':
return parse_time_to_timestamp($input);
default:
return presence(get_string($input));
}
Expand Down Expand Up @@ -1727,6 +1729,11 @@ function parse_time_to_carbon($value)
}
}

function parse_time_to_timestamp(mixed $value): ?int
{
return parse_time_to_carbon($value)?->timestamp;
}

function format_duration_for_display(int $seconds)
{
return floor($seconds / 60).':'.str_pad((string) ($seconds % 60), 2, '0', STR_PAD_LEFT);
Expand Down
10 changes: 10 additions & 0 deletions resources/views/docs/_structures/forum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div id="forum-object" data-unique="forum-object"></div>

## Forum

Field | Type | Notes
------------|---------------------------|-------
id | integer |
name | string |
description | string |
subforums | [Forum](#forum-object)[]? | Maximum 2 layers of subforums from the top-level Forum
3 changes: 2 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,10 @@
Route::group(['as' => 'forum.', 'namespace' => 'Forum'], function () {
Route::group(['prefix' => 'forums'], function () {
Route::post('topics/{topic}/reply', 'TopicsController@reply')->name('topics.reply');
Route::resource('topics', 'TopicsController', ['only' => ['show', 'store', 'update']]);
Route::resource('topics', 'TopicsController', ['only' => ['index', 'show', 'store', 'update']]);
Route::resource('posts', 'PostsController', ['only' => ['update']]);
});
Route::resource('forums', 'ForumsController', ['only' => ['index', 'show']]);
});
Route::resource('matches', 'MatchesController', ['only' => ['index', 'show']]);

Expand Down
48 changes: 48 additions & 0 deletions tests/api_routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,22 @@
"forum.write"
]
},
{
"uri": "api/v2/forums/topics",
"methods": [
"GET",
"HEAD"
],
"controller": "App\\Http\\Controllers\\Forum\\TopicsController@index",
"middlewares": [
"App\\Http\\Middleware\\ThrottleRequests:1200,1,api:",
"App\\Http\\Middleware\\RequireScopes",
"App\\Http\\Middleware\\RequireScopes:public"
],
"scopes": [
"public"
]
},
{
"uri": "api/v2/forums/topics/{topic}",
"methods": [
Expand Down Expand Up @@ -783,6 +799,38 @@
"forum.write"
]
},
{
"uri": "api/v2/forums",
"methods": [
"GET",
"HEAD"
],
"controller": "App\\Http\\Controllers\\Forum\\ForumsController@index",
"middlewares": [
"App\\Http\\Middleware\\ThrottleRequests:1200,1,api:",
"App\\Http\\Middleware\\RequireScopes",
"App\\Http\\Middleware\\RequireScopes:public"
],
"scopes": [
"public"
]
},
{
"uri": "api/v2/forums/{forum}",
"methods": [
"GET",
"HEAD"
],
"controller": "App\\Http\\Controllers\\Forum\\ForumsController@show",
"middlewares": [
"App\\Http\\Middleware\\ThrottleRequests:1200,1,api:",
"App\\Http\\Middleware\\RequireScopes",
"App\\Http\\Middleware\\RequireScopes:public"
],
"scopes": [
"public"
]
},
{
"uri": "api/v2/matches",
"methods": [
Expand Down

0 comments on commit cb80e1b

Please sign in to comment.