diff --git a/lib/Controller/ReactionController.php b/lib/Controller/ReactionController.php index 8ba20cc8c40..3b0df4e40fb 100644 --- a/lib/Controller/ReactionController.php +++ b/lib/Controller/ReactionController.php @@ -30,6 +30,7 @@ use OCA\Talk\Exceptions\ReactionAlreadyExistsException; use OCA\Talk\Exceptions\ReactionNotSupportedException; use OCA\Talk\Exceptions\ReactionOutOfContextException; +use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; @@ -67,12 +68,19 @@ public function __construct( * 400: Adding reaction is not possible * 404: Message not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::CHAT)] #[RequireReadWriteConversation] public function react(int $messageId, string $reaction): DataResponse { + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController::class); + return $proxy->react($this->room, $this->participant, $messageId, $reaction, $this->getResponseFormat()); + } + try { $this->reactionManager->addReactionMessage( $this->getRoom(), @@ -105,12 +113,19 @@ public function react(int $messageId, string $reaction): DataResponse { * 400: Deleting reaction is not possible * 404: Message not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::CHAT)] #[RequireReadWriteConversation] public function delete(int $messageId, string $reaction): DataResponse { + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController::class); + return $proxy->delete($this->room, $this->participant, $messageId, $reaction, $this->getResponseFormat()); + } + try { $this->reactionManager->deleteReactionMessage( $this->getRoom(), @@ -140,10 +155,17 @@ public function delete(int $messageId, string $reaction): DataResponse { * 200: Reactions returned * 404: Message or reaction not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function getReactions(int $messageId, ?string $reaction): DataResponse { + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ReactionController::class); + return $proxy->getReactions($this->room, $this->participant, $messageId, $reaction, $this->getResponseFormat()); + } + try { // Verify that messageId is part of the room $this->reactionManager->getCommentToReact($this->getRoom(), (string) $messageId); @@ -156,12 +178,11 @@ public function getReactions(int $messageId, ?string $reaction): DataResponse { return new DataResponse($this->formatReactions($reactions), Http::STATUS_OK); } - /** * @param array $reactions * @return array|\stdClass */ - public function formatReactions(array $reactions): array|\stdClass { + protected function formatReactions(array $reactions): array|\stdClass { if ($this->getResponseFormat() === 'json' && empty($reactions)) { // Cheating here to make sure the reactions array is always a // JSON object on the API, even when there is no reaction at all. diff --git a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php index 43592df6d20..dd5cde00f0d 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php @@ -41,7 +41,7 @@ */ class AvatarController { public function __construct( - protected ProxyRequest $proxy, + protected ProxyRequest $proxy, ) { } diff --git a/lib/Federation/Proxy/TalkV1/Controller/ReactionController.php b/lib/Federation/Proxy/TalkV1/Controller/ReactionController.php new file mode 100644 index 00000000000..2ed500dcfae --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Controller/ReactionController.php @@ -0,0 +1,177 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Federation\Proxy\TalkV1\Controller; + +use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest; +use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; +use OCA\Talk\Participant; +use OCA\Talk\ResponseDefinitions; +use OCA\Talk\Room; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; + +/** + * @psalm-import-type TalkReaction from ResponseDefinitions + */ +class ReactionController { + public function __construct( + protected ProxyRequest $proxy, + protected UserConverter $userConverter, + ) { + } + + /** + * Add a reaction to a message + * + * @param int $messageId ID of the message + * @psalm-param non-negative-int $messageId + * @param string $reaction Emoji to add + * @return DataResponse|\stdClass, array{}>|DataResponse, array{}> + * + * 200: Reaction already existed + * 201: Reaction added successfully + * 400: Adding reaction is not possible + * 404: Message not found + */ + public function react(Room $room, Participant $participant, int $messageId, string $reaction, string $format): DataResponse { + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId, + [ + 'reaction' => $reaction, + ], + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK && $statusCode !== Http::STATUS_CREATED) { + if (!in_array($statusCode, [ + Http::STATUS_BAD_REQUEST, + Http::STATUS_NOT_FOUND, + ], true)) { + $statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + } + return new DataResponse([], $statusCode); + } + + /** @var array $data */ + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED, Http::STATUS_OK]); + $data = $this->userConverter->convertReactionsList($room, $data); + + return new DataResponse($this->formatReactions($format, $data), $statusCode); + } + + /** + * Delete a reaction from a message + * + * @param int $messageId ID of the message + * @psalm-param non-negative-int $messageId + * @param string $reaction Emoji to remove + * @return DataResponse|\stdClass, array{}>|DataResponse, array{}> + * + * 200: Reaction deleted successfully + * 400: Deleting reaction is not possible + * 404: Message not found + */ + public function delete(Room $room, Participant $participant, int $messageId, string $reaction, string $format): DataResponse { + $proxy = $this->proxy->delete( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId, + [ + 'reaction' => $reaction, + ], + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK) { + if (!in_array($statusCode, [ + Http::STATUS_BAD_REQUEST, + Http::STATUS_NOT_FOUND, + ], true)) { + $statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + } + return new DataResponse([], $statusCode); + } + + /** @var array $data */ + $data = $this->proxy->getOCSData($proxy); + $data = $this->userConverter->convertReactionsList($room, $data); + + return new DataResponse($this->formatReactions($format, $data), $statusCode); + } + + + /** + * Get a list of reactions for a message + * + * @param int $messageId ID of the message + * @psalm-param non-negative-int $messageId + * @param string|null $reaction Emoji to filter + * @return DataResponse|\stdClass, array{}>|DataResponse, array{}> + * + * 200: Reactions returned + * 404: Message or reaction not found + */ + public function getReactions(Room $room, Participant $participant, int $messageId, ?string $reaction, string $format): DataResponse { + $proxy = $this->proxy->get( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/reaction/' . $room->getRemoteToken() . '/' . $messageId, + $reaction === null ? [] : [ + 'reaction' => $reaction, + ], + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK) { + if ($statusCode !== Http::STATUS_NOT_FOUND) { + $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + } + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + /** @var array $data */ + $data = $this->proxy->getOCSData($proxy); + $data = $this->userConverter->convertReactionsList($room, $data); + + return new DataResponse($this->formatReactions($format, $data), $statusCode); + } + + /** + * @param array $reactions + * @return array|\stdClass + */ + protected function formatReactions(string $format, array $reactions): array|\stdClass { + if ($format === 'json' && empty($reactions)) { + // Cheating here to make sure the reactions array is always a + // JSON object on the API, even when there is no reaction at all. + return new \stdClass(); + } + + return $reactions; + } +} diff --git a/lib/Federation/Proxy/TalkV1/UserConverter.php b/lib/Federation/Proxy/TalkV1/UserConverter.php index 3fb31c10737..44ad8dc2489 100644 --- a/lib/Federation/Proxy/TalkV1/UserConverter.php +++ b/lib/Federation/Proxy/TalkV1/UserConverter.php @@ -34,6 +34,7 @@ /** * @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions + * @psalm-import-type TalkReaction from ResponseDefinitions */ class UserConverter { /** @@ -151,6 +152,30 @@ public function convertMessages(Room $room, array $messages): array { ); } + /** + * @param Room $room + * @param TalkReaction[] $reactions + * @return TalkReaction[] + */ + protected function convertReactions(Room $room, array $reactions): array { + return array_map( + fn (array $reaction): array => $this->convertAttendee($room, $reaction, 'actorType', 'actorId', 'actorDisplayName'), + $reactions + ); + } + + /** + * @param Room $room + * @param array $reactionsList + * @return array + */ + public function convertReactionsList(Room $room, array $reactionsList): array { + return array_map( + fn (array $reactions): array => $this->convertReactions($room, $reactions), + $reactionsList + ); + } + /** * @return array */ diff --git a/lib/Room.php b/lib/Room.php index 5d6ebd62235..3fdaa081f09 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -398,6 +398,14 @@ public function getRemoteServer(): string { return $this->remoteServer; } + /** + * Whether the conversation is a "proxy conversation" or the original hosted conversation + * @return bool + */ + public function isFederatedConversation(): bool { + return $this->remoteServer !== ''; + } + public function getRemoteToken(): string { return $this->remoteToken; }