diff --git a/lib/Controller/ForumUserController.php b/lib/Controller/ForumUserController.php index b3be140..2e3f9aa 100644 --- a/lib/Controller/ForumUserController.php +++ b/lib/Controller/ForumUserController.php @@ -49,18 +49,28 @@ class ForumUserController extends OCSController { } /** - * Get a single forum user + * Get forum user by Nextcloud user ID + * Special case: use "me" to get current user * - * @param int $id Forum user ID + * @param string $userId Nextcloud user ID or "me" for current user * @return DataResponse, array{}> * * 200: Forum user returned */ #[NoAdminRequired] - #[ApiRoute(verb: 'GET', url: '/api/users/{id}')] - public function show(int $id): DataResponse { + #[ApiRoute(verb: 'GET', url: '/api/users/{userId}')] + public function show(string $userId): DataResponse { try { - $user = $this->forumUserMapper->find($id); + // Handle "me" as special case for current user + if ($userId === 'me') { + $currentUser = $this->userSession->getUser(); + if (!$currentUser) { + return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); + } + $userId = $currentUser->getUID(); + } + + $user = $this->forumUserMapper->findByUserId($userId); return new DataResponse($user->jsonSerialize()); } catch (DoesNotExistException $e) { return new DataResponse(['error' => 'Forum user not found'], Http::STATUS_NOT_FOUND); @@ -70,54 +80,6 @@ class ForumUserController extends OCSController { } } - /** - * Get forum user by Nextcloud user ID - * - * @param string $userId Nextcloud user ID - * @return DataResponse, array{}> - * - * 200: Forum user returned - */ - #[NoAdminRequired] - #[ApiRoute(verb: 'GET', url: '/api/users/by-uid/{userId}')] - public function byUserId(string $userId): DataResponse { - try { - $user = $this->forumUserMapper->findByUserId($userId); - return new DataResponse($user->jsonSerialize()); - } catch (DoesNotExistException $e) { - return new DataResponse(['error' => 'Forum user not found'], Http::STATUS_NOT_FOUND); - } catch (\Exception $e) { - $this->logger->error('Error fetching forum user by user ID: ' . $e->getMessage()); - return new DataResponse(['error' => 'Failed to fetch forum user'], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } - - /** - * Get current user's forum profile - * - * @return DataResponse, array{}> - * - * 200: Current user's forum profile returned - */ - #[NoAdminRequired] - #[ApiRoute(verb: 'GET', url: '/api/current-user')] - public function me(): DataResponse { - try { - $user = $this->userSession->getUser(); - if (!$user) { - return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); - } - - $forumUser = $this->forumUserMapper->findByUserId($user->getUID()); - return new DataResponse($forumUser->jsonSerialize()); - } catch (DoesNotExistException $e) { - return new DataResponse(['error' => 'Forum user not found'], Http::STATUS_NOT_FOUND); - } catch (\Exception $e) { - $this->logger->error('Error fetching current user: ' . $e->getMessage()); - return new DataResponse(['error' => 'Failed to fetch current user'], Http::STATUS_INTERNAL_SERVER_ERROR); - } - } - /** * Create a new forum user * @@ -148,17 +110,26 @@ class ForumUserController extends OCSController { /** * Update a forum user * - * @param int $id Forum user ID + * @param string $userId Nextcloud user ID or "me" for current user * @param int|null $postCount Post count * @return DataResponse, array{}> * * 200: Forum user updated */ #[NoAdminRequired] - #[ApiRoute(verb: 'PUT', url: '/api/users/{id}')] - public function update(int $id, ?int $postCount = null): DataResponse { + #[ApiRoute(verb: 'PUT', url: '/api/users/{userId}')] + public function update(string $userId, ?int $postCount = null): DataResponse { try { - $user = $this->forumUserMapper->find($id); + // Handle "me" as special case for current user + if ($userId === 'me') { + $currentUser = $this->userSession->getUser(); + if (!$currentUser) { + return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); + } + $userId = $currentUser->getUID(); + } + + $user = $this->forumUserMapper->findByUserId($userId); if ($postCount !== null) { $user->setPostCount($postCount); @@ -179,16 +150,25 @@ class ForumUserController extends OCSController { /** * Delete a forum user * - * @param int $id Forum user ID + * @param string $userId Nextcloud user ID or "me" for current user * @return DataResponse * * 200: Forum user deleted */ #[NoAdminRequired] - #[ApiRoute(verb: 'DELETE', url: '/api/users/{id}')] - public function destroy(int $id): DataResponse { + #[ApiRoute(verb: 'DELETE', url: '/api/users/{userId}')] + public function destroy(string $userId): DataResponse { try { - $user = $this->forumUserMapper->find($id); + // Handle "me" as special case for current user + if ($userId === 'me') { + $currentUser = $this->userSession->getUser(); + if (!$currentUser) { + return new DataResponse(['error' => 'User not authenticated'], Http::STATUS_UNAUTHORIZED); + } + $userId = $currentUser->getUID(); + } + + $user = $this->forumUserMapper->findByUserId($userId); $this->forumUserMapper->delete($user); return new DataResponse(['success' => true]); } catch (DoesNotExistException $e) { diff --git a/lib/Controller/PostController.php b/lib/Controller/PostController.php index aeeb2d2..ec63180 100644 --- a/lib/Controller/PostController.php +++ b/lib/Controller/PostController.php @@ -95,6 +95,53 @@ class PostController extends OCSController { } } + /** + * Get posts by author + * + * @param string $authorId Author user ID + * @param int $limit Maximum number of posts to return + * @param int $offset Offset for pagination + * @return DataResponse>, array{}> + * + * 200: Posts returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/users/{authorId}/posts')] + public function byAuthor(string $authorId, int $limit = 50, int $offset = 0): DataResponse { + try { + $posts = $this->postMapper->findByAuthorId($authorId, $limit, $offset); + + // Prefetch BBCodes once for all posts to avoid repeated queries + $bbcodes = $this->bbCodeMapper->findAllEnabled(); + + // Fetch reactions for all posts at once (performance optimization) + $postIds = array_map(fn ($p) => $p->getId(), $posts); + $reactions = $this->reactionMapper->findByPostIds($postIds); + + // Group reactions by post ID + $reactionsByPostId = []; + foreach ($reactions as $reaction) { + $postId = $reaction->getPostId(); + if (!isset($reactionsByPostId[$postId])) { + $reactionsByPostId[$postId] = []; + } + $reactionsByPostId[$postId][] = $reaction; + } + + // Get current user ID to mark user's reactions + $currentUserId = $this->userSession->getUser()?->getUID(); + + // Enrich posts with content and reactions + return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId) { + $postReactions = $reactionsByPostId[$p->getId()] ?? []; + return Post::enrichPostContent($p, $bbcodes, $postReactions, $currentUserId); + }, $posts)); + } catch (\Exception $e) { + $this->logger->error('Error fetching posts by author: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch posts'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Get a single post * diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index 64d7c1c..25b1525 100644 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -80,6 +80,28 @@ class ThreadController extends OCSController { } } + /** + * Get threads by author + * + * @param string $authorId Author user ID + * @param int $limit Maximum number of threads to return + * @param int $offset Offset for pagination + * @return DataResponse>, array{}> + * + * 200: Threads returned + */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/users/{authorId}/threads')] + public function byAuthor(string $authorId, int $limit = 50, int $offset = 0): DataResponse { + try { + $threads = $this->threadMapper->findByAuthorId($authorId, $limit, $offset); + return new DataResponse(array_map(fn ($t) => Thread::enrichThread($t), $threads)); + } catch (\Exception $e) { + $this->logger->error('Error fetching threads by author: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch threads'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + /** * Get a single thread * diff --git a/lib/Db/ThreadMapper.php b/lib/Db/ThreadMapper.php index 4867645..001e0ce 100644 --- a/lib/Db/ThreadMapper.php +++ b/lib/Db/ThreadMapper.php @@ -176,6 +176,26 @@ class ThreadMapper extends QBMapper { return (int)($row['count'] ?? 0); } + /** + * @return array + */ + public function findByAuthorId(string $authorId, int $limit = 50, int $offset = 0): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('author_id', $qb->createNamedParameter($authorId, IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->isNull('deleted_at') + ) + ->orderBy('created_at', 'DESC') + ->setMaxResults($limit) + ->setFirstResult($offset); + return $this->findEntities($qb); + } + /** * Search threads by title and first post content * diff --git a/lib/Db/UserStats.php b/lib/Db/UserStats.php new file mode 100644 index 0000000..5242dc5 --- /dev/null +++ b/lib/Db/UserStats.php @@ -0,0 +1,61 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Db; + +use JsonSerializable; +use OCP\AppFramework\Db\Entity; + +/** + * @method string getUserId() + * @method void setUserId(string $userId) + * @method int getPostCount() + * @method void setPostCount(int $postCount) + * @method int getThreadCount() + * @method void setThreadCount(int $threadCount) + * @method int|null getLastPostAt() + * @method void setLastPostAt(?int $lastPostAt) + * @method int|null getDeletedAt() + * @method void setDeletedAt(?int $deletedAt) + * @method int getCreatedAt() + * @method void setCreatedAt(int $createdAt) + * @method int getUpdatedAt() + * @method void setUpdatedAt(int $updatedAt) + */ +class UserStats extends Entity implements JsonSerializable { + protected string $userId = ''; + protected int $postCount = 0; + protected int $threadCount = 0; + protected ?int $lastPostAt = null; + protected ?int $deletedAt = null; + protected int $createdAt = 0; + protected int $updatedAt = 0; + + public function __construct() { + // User ID is the primary key, not an auto-increment id + $this->addType('userId', 'string'); + $this->addType('postCount', 'integer'); + $this->addType('threadCount', 'integer'); + $this->addType('lastPostAt', 'integer'); + $this->addType('deletedAt', 'integer'); + $this->addType('createdAt', 'integer'); + $this->addType('updatedAt', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'userId' => $this->userId, + 'postCount' => $this->postCount, + 'threadCount' => $this->threadCount, + 'lastPostAt' => $this->lastPostAt, + 'deletedAt' => $this->deletedAt, + 'isDeleted' => $this->deletedAt !== null, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } +} diff --git a/openapi.json b/openapi.json index e4bdb69..85cb352 100644 --- a/openapi.json +++ b/openapi.json @@ -3534,10 +3534,10 @@ } } }, - "/ocs/v2.php/apps/forum/api/users/{id}": { + "/ocs/v2.php/apps/forum/api/users/{userId}": { "get": { "operationId": "forum_user-show", - "summary": "Get a single forum user", + "summary": "Get forum user by Nextcloud user ID Special case: use \"me\" to get current user", "tags": [ "forum_user" ], @@ -3551,13 +3551,12 @@ ], "parameters": [ { - "name": "id", + "name": "userId", "in": "path", - "description": "Forum user ID", + "description": "Nextcloud user ID or \"me\" for current user", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { @@ -3670,13 +3669,12 @@ }, "parameters": [ { - "name": "id", + "name": "userId", "in": "path", - "description": "Forum user ID", + "description": "Nextcloud user ID or \"me\" for current user", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { @@ -3770,13 +3768,12 @@ ], "parameters": [ { - "name": "id", + "name": "userId", "in": "path", - "description": "Forum user ID", + "description": "Nextcloud user ID or \"me\" for current user", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } }, { @@ -3860,199 +3857,6 @@ } } }, - "/ocs/v2.php/apps/forum/api/users/by-uid/{userId}": { - "get": { - "operationId": "forum_user-by-user-id", - "summary": "Get forum user by Nextcloud user ID", - "tags": [ - "forum_user" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "userId", - "in": "path", - "description": "Nextcloud user ID", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Forum user returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, - "/ocs/v2.php/apps/forum/api/current-user": { - "get": { - "operationId": "forum_user-me", - "summary": "Get current user's forum profile", - "tags": [ - "forum_user" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Current user's forum profile returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "additionalProperties": { - "type": "object" - } - } - } - } - } - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, "/ocs/v2.php/apps/forum/api/threads/{threadId}/posts": { "get": { "operationId": "post-by-thread", @@ -4178,6 +3982,130 @@ } } }, + "/ocs/v2.php/apps/forum/api/users/{authorId}/posts": { + "get": { + "operationId": "post-by-author", + "summary": "Get posts by author", + "tags": [ + "post" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "authorId", + "in": "path", + "description": "Author user ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of posts to return", + "schema": { + "type": "integer", + "format": "int64", + "default": 50 + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for pagination", + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Posts returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/forum/api/posts/{id}": { "get": { "operationId": "post-show", @@ -7189,6 +7117,130 @@ } } }, + "/ocs/v2.php/apps/forum/api/users/{authorId}/threads": { + "get": { + "operationId": "thread-by-author", + "summary": "Get threads by author", + "tags": [ + "thread" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "authorId", + "in": "path", + "description": "Author user ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of threads to return", + "schema": { + "type": "integer", + "format": "int64", + "default": 50 + } + }, + { + "name": "offset", + "in": "query", + "description": "Offset for pagination", + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Threads returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/forum/api/threads/{id}": { "get": { "operationId": "thread-show", diff --git a/src/components/PostCard.vue b/src/components/PostCard.vue index 0d5754f..ecc32c4 100644 --- a/src/components/PostCard.vue +++ b/src/components/PostCard.vue @@ -2,11 +2,20 @@
- +
- + + {{ post.authorDisplayName || post.authorId }} + + {{ post.authorDisplayName || post.authorId }}