diff --git a/docs/poll.md b/docs/poll.md index 8d41e105f21..7ea130a3adf 100644 --- a/docs/poll.md +++ b/docs/poll.md @@ -4,6 +4,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ## Create a poll in a conversation +* Federation capability: `federation-v1` * Method: `POST` * Endpoint: `/poll/{token}` * Data: @@ -31,6 +32,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ## Get state or result of a poll +* Federation capability: `federation-v1` * Method: `GET` * Endpoint: `/poll/{token}/{pollId}` @@ -48,6 +50,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ## Vote on a poll +* Federation capability: `federation-v1` * Method: `POST` * Endpoint: `/poll/{token}/{pollId}` * Data: @@ -72,6 +75,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` ## Close a poll +* Federation capability: `federation-v1` * Method: `DELETE` * Endpoint: `/poll/{token}/{pollId}` diff --git a/lib/Controller/PollController.php b/lib/Controller/PollController.php index 762529e08e3..ae94b66ae4f 100644 --- a/lib/Controller/PollController.php +++ b/lib/Controller/PollController.php @@ -30,6 +30,7 @@ use JsonException; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Exceptions\WrongPermissionsException; +use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; @@ -80,12 +81,19 @@ public function __construct( * 201: Poll created successfully * 400: Creating poll is not possible */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::CHAT)] #[RequireReadWriteConversation] public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse { + if ($this->room->getRemoteServer() !== '') { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class); + return $proxy->createPoll($this->room, $this->participant, $question, $options, $resultMode, $maxVotes); + } + if ($this->room->getType() !== Room::TYPE_GROUP && $this->room->getType() !== Room::TYPE_PUBLIC) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -140,10 +148,17 @@ public function createPoll(string $question, array $options, int $resultMode, in * 200: Poll returned * 404: Poll not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function showPoll(int $pollId): DataResponse { + if ($this->room->getRemoteServer() !== '') { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class); + return $proxy->showPoll($this->room, $this->participant, $pollId); + } + try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (DoesNotExistException $e) { @@ -171,10 +186,17 @@ public function showPoll(int $pollId): DataResponse { * 400: Voting is not possible * 404: Poll not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function votePoll(int $pollId, array $optionIds = []): DataResponse { + if ($this->room->getRemoteServer() !== '') { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class); + return $proxy->votePoll($this->room, $this->participant, $pollId, $optionIds); + } + try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (\Exception $e) { @@ -225,10 +247,17 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse { * 403: Missing permissions to close poll * 404: Poll not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function closePoll(int $pollId): DataResponse { + if ($this->room->getRemoteServer() !== '') { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class); + return $proxy->closePoll($this->room, $this->participant, $pollId); + } + try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (\Exception $e) { diff --git a/lib/Federation/Proxy/TalkV1/Controller/PollController.php b/lib/Federation/Proxy/TalkV1/Controller/PollController.php new file mode 100644 index 00000000000..442b126772a --- /dev/null +++ b/lib/Federation/Proxy/TalkV1/Controller/PollController.php @@ -0,0 +1,181 @@ + + * + * @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\Exceptions\CannotReachRemoteException; +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 TalkPoll from ResponseDefinitions + */ +class PollController { + public function __construct( + protected ProxyRequest $proxy, + protected UserConverter $userConverter, + ) { + } + + /** + * @return DataResponse|DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Poll returned + * 404: Poll not found + * + * @see \OCA\Talk\Controller\PollController::showPoll() + */ + public function showPoll(Room $room, Participant $participant, int $pollId): DataResponse { + $proxy = $this->proxy->get( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId, + ); + + if ($proxy->getStatusCode() === Http::STATUS_NOT_FOUND) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + /** @var TalkPoll $data */ + $data = $this->proxy->getOCSData($proxy); + $data = $this->userConverter->convertPoll($room, $data); + + return new DataResponse($data); + } + + /** + * @return DataResponse|DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Voted successfully + * 400: Voting is not possible + * 404: Poll not found + * + * @see \OCA\Talk\Controller\PollController::votePoll() + */ + public function votePoll(Room $room, Participant $participant, int $pollId, array $optionIds): DataResponse { + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId, + ['optionIds' => $optionIds], + ); + + $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 TalkPoll $data */ + $data = $this->proxy->getOCSData($proxy); + $data = $this->userConverter->convertPoll($room, $data); + + return new DataResponse($data); + } + + + /** + * @return DataResponse|DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 201: Poll created successfully + * 400: Creating poll is not possible + * + * @see \OCA\Talk\Controller\PollController::createPoll() + */ + public function createPoll(Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes): DataResponse { + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken(), + [ + 'question' => $question, + 'options' => $options, + 'resultMode' => $resultMode, + 'maxVotes' => $maxVotes, + ], + ); + + if ($proxy->getStatusCode() === Http::STATUS_BAD_REQUEST) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + /** @var TalkPoll $data */ + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED]); + $data = $this->userConverter->convertPoll($room, $data); + + return new DataResponse($data, Http::STATUS_CREATED); + } + + /** + * @return DataResponse|DataResponse, array{}> + * @throws CannotReachRemoteException + * + * 200: Poll closed successfully + * 400: Poll already closed + * 403: Missing permissions to close poll + * 404: Poll not found + * + * @see \OCA\Talk\Controller\PollController::closePoll() + */ + public function closePoll(Room $room, Participant $participant, int $pollId): DataResponse { + $proxy = $this->proxy->delete( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId, + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK) { + if (!in_array($statusCode, [ + Http::STATUS_BAD_REQUEST, + Http::STATUS_FORBIDDEN, + Http::STATUS_NOT_FOUND, + ], true)) { + $statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + } + return new DataResponse([], $statusCode); + } + + /** @var TalkPoll $data */ + $data = $this->proxy->getOCSData($proxy); + $data = $this->userConverter->convertPoll($room, $data); + + return new DataResponse($data); + } +} diff --git a/lib/Federation/Proxy/TalkV1/UserConverter.php b/lib/Federation/Proxy/TalkV1/UserConverter.php index 44ad8dc2489..a1af19fdb4f 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 TalkPoll from ResponseDefinitions * @psalm-import-type TalkReaction from ResponseDefinitions */ class UserConverter { @@ -152,6 +153,22 @@ public function convertMessages(Room $room, array $messages): array { ); } + /** + * @param Room $room + * @param TalkPoll $poll + * @return TalkPoll + */ + public function convertPoll(Room $room, array $poll): array { + $poll = $this->convertAttendee($room, $poll, 'actorType', 'actorId', 'actorDisplayName'); + if (isset($poll['details'])) { + $poll['details'] = array_map( + fn (array $vote): array => $this->convertAttendee($room, $vote, 'actorType', 'actorId', 'actorDisplayName'), + $poll['details'] + ); + } + return $poll; + } + /** * @param Room $room * @param TalkReaction[] $reactions diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue index 181cda13bc7..7c15dfcea8a 100644 --- a/src/components/NewMessage/NewMessage.vue +++ b/src/components/NewMessage/NewMessage.vue @@ -420,7 +420,6 @@ export default { canCreatePoll() { return !this.isOneToOne && !this.noChatPermission && this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF - && (!supportFederationV1 || !this.conversation.remoteServer) }, currentConversationIsJoined() { @@ -459,7 +458,7 @@ export default { }, showAttachmentsMenu() { - return this.canShareFiles && !this.broadcast && !this.upload && !this.messageToEdit + return (this.canUploadFiles || this.canShareFiles || this.canCreatePoll) && !this.broadcast && !this.upload && !this.messageToEdit }, showAudioRecorder() { diff --git a/src/components/NewMessage/NewMessageAttachments.vue b/src/components/NewMessage/NewMessageAttachments.vue index 778e25fbfcc..654774b4647 100644 --- a/src/components/NewMessage/NewMessageAttachments.vue +++ b/src/components/NewMessage/NewMessageAttachments.vue @@ -26,6 +26,7 @@ :container="container" :boundaries-element="boundariesElement" :disabled="disabled" + :force-menu="true" :aria-label="t('spreed', 'Share files to the conversation')" :aria-haspopup="true">