* * @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\Controller; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Exceptions\WrongPermissionsException; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; use OCA\Talk\Middleware\Attribute\RequireReadWriteConversation; use OCA\Talk\Model\Poll; use OCA\Talk\Model\Vote; use OCA\Talk\Room; use OCA\Talk\Service\AttachmentService; use OCA\Talk\Service\PollService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\Exception; use OCP\IRequest; use Psr\Log\LoggerInterface; class PollController extends AEnvironmentAwareController { protected ChatManager $chatManager; protected PollService $pollService; protected AttachmentService $attachmentService; protected ITimeFactory $timeFactory; protected LoggerInterface $logger; public function __construct( string $appName, IRequest $request, ChatManager $chatManager, PollService $pollService, AttachmentService $attachmentService, ITimeFactory $timeFactory, LoggerInterface $logger, ) { parent::__construct($appName, $request); $this->pollService = $pollService; $this->attachmentService = $attachmentService; $this->chatManager = $chatManager; $this->timeFactory = $timeFactory; $this->logger = $logger; } #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] #[RequirePermission(permission: RequirePermission::CHAT)] #[RequireReadWriteConversation] public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse { if ($this->room->getType() !== Room::TYPE_GROUP && $this->room->getType() !== Room::TYPE_PUBLIC) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } $attendee = $this->participant->getAttendee(); try { $poll = $this->pollService->createPoll( $this->room->getId(), $attendee->getActorType(), $attendee->getActorId(), $attendee->getDisplayName(), $question, $options, $resultMode, $maxVotes ); } catch (\Exception $e) { $this->logger->error('Error creating poll', ['exception' => $e]); return new DataResponse([], Http::STATUS_BAD_REQUEST); } $message = json_encode([ 'message' => 'object_shared', 'parameters' => [ 'objectType' => 'talk-poll', 'objectId' => $poll->getId(), 'metaData' => [ 'type' => 'talk-poll', 'id' => $poll->getId(), 'name' => $question, ] ], ], JSON_THROW_ON_ERROR); try { $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); } return new DataResponse($this->renderPoll($poll, []), Http::STATUS_CREATED); } #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function showPoll(int $pollId): DataResponse { try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (DoesNotExistException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } $votedSelf = $this->pollService->getVotesForActor($this->participant, $poll); $detailedVotes = []; if ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) { $detailedVotes = $this->pollService->getVotes($poll); } return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes)); } /** * @param int $pollId * @param int[] $optionIds * @return DataResponse */ #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function votePoll(int $pollId, array $optionIds = []): DataResponse { try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (\Exception $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } if ($poll->getStatus() === Poll::STATUS_CLOSED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } try { $votedSelf = $this->pollService->votePoll($this->participant, $poll, $optionIds); } catch (\RuntimeException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } if ($poll->getResultMode() === Poll::MODE_PUBLIC) { $attendee = $this->participant->getAttendee(); try { $message = json_encode([ 'message' => 'poll_voted', 'parameters' => [ 'poll' => [ 'type' => 'talk-poll', 'id' => $poll->getId(), 'name' => $poll->getQuestion(), ], ], ], JSON_THROW_ON_ERROR); $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), false); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); } } return new DataResponse($this->renderPoll($poll, $votedSelf)); } #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] public function closePoll(int $pollId): DataResponse { try { $poll = $this->pollService->getPoll($this->room->getId(), $pollId); } catch (\Exception $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } if ($poll->getStatus() === Poll::STATUS_CLOSED) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } $poll->setStatus(Poll::STATUS_CLOSED); try { $this->pollService->updatePoll($this->participant, $poll); } catch (WrongPermissionsException $e) { return new DataResponse([], Http::STATUS_FORBIDDEN); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); } $attendee = $this->participant->getAttendee(); try { $message = json_encode([ 'message' => 'poll_closed', 'parameters' => [ 'poll' => [ 'type' => 'talk-poll', 'id' => $poll->getId(), 'name' => $poll->getQuestion(), ], ], ], JSON_THROW_ON_ERROR); $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true); } catch (\Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); } $detailedVotes = []; if ($poll->getResultMode() === Poll::MODE_PUBLIC) { $detailedVotes = $this->pollService->getVotes($poll); } $votedSelf = $this->pollService->getVotesForActor($this->participant, $poll); return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes)); } protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array { $data = $poll->asArray(); unset($data['roomId']); $canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC; if (!$canSeeSummary && $poll->getStatus() === Poll::STATUS_OPEN) { $data['votes'] = []; if ($this->participant->hasModeratorPermissions() || ($poll->getActorType() === $this->participant->getAttendee()->getActorType() && $poll->getActorId() === $this->participant->getAttendee()->getActorId())) { // Allow moderators and the author to see the number of voters, // So they know when to close the poll. } else { $data['numVoters'] = 0; } } elseif ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) { $data['details'] = array_map(static fn (Vote $vote) => $vote->asArray(), $detailedVotes); } $data['votedSelf'] = array_map(static fn (Vote $vote) => $vote->getOptionId(), $votedSelf); return $data; } }