You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
9.6 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
  5. *
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Kate Döen <kate.doeen@nextcloud.com>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OCA\Talk\Controller;
  26. use JsonException;
  27. use OCA\Talk\Chat\ChatManager;
  28. use OCA\Talk\Exceptions\WrongPermissionsException;
  29. use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
  30. use OCA\Talk\Middleware\Attribute\RequireParticipant;
  31. use OCA\Talk\Middleware\Attribute\RequirePermission;
  32. use OCA\Talk\Middleware\Attribute\RequireReadWriteConversation;
  33. use OCA\Talk\Model\Poll;
  34. use OCA\Talk\Model\Vote;
  35. use OCA\Talk\ResponseDefinitions;
  36. use OCA\Talk\Room;
  37. use OCA\Talk\Service\AttachmentService;
  38. use OCA\Talk\Service\PollService;
  39. use OCP\AppFramework\Db\DoesNotExistException;
  40. use OCP\AppFramework\Http;
  41. use OCP\AppFramework\Http\Attribute\PublicPage;
  42. use OCP\AppFramework\Http\DataResponse;
  43. use OCP\AppFramework\Utility\ITimeFactory;
  44. use OCP\DB\Exception;
  45. use OCP\IRequest;
  46. use Psr\Log\LoggerInterface;
  47. /**
  48. * @psalm-import-type TalkPoll from ResponseDefinitions
  49. */
  50. class PollController extends AEnvironmentAwareController {
  51. public function __construct(
  52. string $appName,
  53. IRequest $request,
  54. protected ChatManager $chatManager,
  55. protected PollService $pollService,
  56. protected AttachmentService $attachmentService,
  57. protected ITimeFactory $timeFactory,
  58. protected LoggerInterface $logger,
  59. ) {
  60. parent::__construct($appName, $request);
  61. }
  62. /**
  63. * Create a poll
  64. *
  65. * @param string $question Question of the poll
  66. * @param string[] $options Options of the poll
  67. * @param int $resultMode Mode how the results will be shown
  68. * @param int $maxVotes Number of maximum votes per voter
  69. * @return DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, array{}>
  70. *
  71. * 201: Poll created successfully
  72. * 400: Creating poll is not possible
  73. */
  74. #[PublicPage]
  75. #[RequireModeratorOrNoLobby]
  76. #[RequireParticipant]
  77. #[RequirePermission(permission: RequirePermission::CHAT)]
  78. #[RequireReadWriteConversation]
  79. public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
  80. if ($this->room->getType() !== Room::TYPE_GROUP
  81. && $this->room->getType() !== Room::TYPE_PUBLIC) {
  82. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  83. }
  84. $attendee = $this->participant->getAttendee();
  85. try {
  86. $poll = $this->pollService->createPoll(
  87. $this->room->getId(),
  88. $attendee->getActorType(),
  89. $attendee->getActorId(),
  90. $attendee->getDisplayName(),
  91. $question,
  92. $options,
  93. $resultMode,
  94. $maxVotes
  95. );
  96. } catch (\Exception $e) {
  97. $this->logger->error('Error creating poll', ['exception' => $e]);
  98. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  99. }
  100. $message = json_encode([
  101. 'message' => 'object_shared',
  102. 'parameters' => [
  103. 'objectType' => 'talk-poll',
  104. 'objectId' => $poll->getId(),
  105. 'metaData' => [
  106. 'type' => 'talk-poll',
  107. 'id' => $poll->getId(),
  108. 'name' => $question,
  109. ]
  110. ],
  111. ], JSON_THROW_ON_ERROR);
  112. try {
  113. $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true);
  114. } catch (\Exception $e) {
  115. $this->logger->error($e->getMessage(), ['exception' => $e]);
  116. }
  117. return new DataResponse($this->renderPoll($poll, []), Http::STATUS_CREATED);
  118. }
  119. /**
  120. * Get a poll
  121. *
  122. * @param int $pollId ID of the poll
  123. * @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
  124. *
  125. * 200: Poll returned
  126. * 404: Poll not found
  127. */
  128. #[PublicPage]
  129. #[RequireModeratorOrNoLobby]
  130. #[RequireParticipant]
  131. public function showPoll(int $pollId): DataResponse {
  132. try {
  133. $poll = $this->pollService->getPoll($this->room->getId(), $pollId);
  134. } catch (DoesNotExistException $e) {
  135. return new DataResponse([], Http::STATUS_NOT_FOUND);
  136. }
  137. $votedSelf = $this->pollService->getVotesForActor($this->participant, $poll);
  138. $detailedVotes = [];
  139. if ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) {
  140. $detailedVotes = $this->pollService->getVotes($poll);
  141. }
  142. return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes));
  143. }
  144. /**
  145. * Vote on a poll
  146. *
  147. * @param int $pollId ID of the poll
  148. * @param int[] $optionIds IDs of the selected options
  149. * @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array<empty>, array{}>
  150. *
  151. * 200: Voted successfully
  152. * 400: Voting is not possible
  153. * 404: Poll not found
  154. */
  155. #[PublicPage]
  156. #[RequireModeratorOrNoLobby]
  157. #[RequireParticipant]
  158. public function votePoll(int $pollId, array $optionIds = []): DataResponse {
  159. try {
  160. $poll = $this->pollService->getPoll($this->room->getId(), $pollId);
  161. } catch (\Exception $e) {
  162. return new DataResponse([], Http::STATUS_NOT_FOUND);
  163. }
  164. if ($poll->getStatus() === Poll::STATUS_CLOSED) {
  165. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  166. }
  167. try {
  168. $votedSelf = $this->pollService->votePoll($this->participant, $poll, $optionIds);
  169. } catch (\RuntimeException $e) {
  170. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  171. }
  172. if ($poll->getResultMode() === Poll::MODE_PUBLIC) {
  173. $attendee = $this->participant->getAttendee();
  174. try {
  175. $message = json_encode([
  176. 'message' => 'poll_voted',
  177. 'parameters' => [
  178. 'poll' => [
  179. 'type' => 'talk-poll',
  180. 'id' => $poll->getId(),
  181. 'name' => $poll->getQuestion(),
  182. ],
  183. ],
  184. ], JSON_THROW_ON_ERROR);
  185. $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), false);
  186. } catch (\Exception $e) {
  187. $this->logger->error($e->getMessage(), ['exception' => $e]);
  188. }
  189. }
  190. return new DataResponse($this->renderPoll($poll, $votedSelf));
  191. }
  192. /**
  193. * Close a poll
  194. *
  195. * @param int $pollId ID of the poll
  196. * @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
  197. *
  198. * 200: Poll closed successfully
  199. * 400: Poll already closed
  200. * 403: Missing permissions to close poll
  201. * 404: Poll not found
  202. */
  203. #[PublicPage]
  204. #[RequireModeratorOrNoLobby]
  205. #[RequireParticipant]
  206. public function closePoll(int $pollId): DataResponse {
  207. try {
  208. $poll = $this->pollService->getPoll($this->room->getId(), $pollId);
  209. } catch (\Exception $e) {
  210. return new DataResponse([], Http::STATUS_NOT_FOUND);
  211. }
  212. if ($poll->getStatus() === Poll::STATUS_CLOSED) {
  213. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  214. }
  215. $poll->setStatus(Poll::STATUS_CLOSED);
  216. try {
  217. $this->pollService->updatePoll($this->participant, $poll);
  218. } catch (WrongPermissionsException $e) {
  219. return new DataResponse([], Http::STATUS_FORBIDDEN);
  220. } catch (Exception $e) {
  221. $this->logger->error($e->getMessage(), ['exception' => $e]);
  222. return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
  223. }
  224. $attendee = $this->participant->getAttendee();
  225. try {
  226. $message = json_encode([
  227. 'message' => 'poll_closed',
  228. 'parameters' => [
  229. 'poll' => [
  230. 'type' => 'talk-poll',
  231. 'id' => $poll->getId(),
  232. 'name' => $poll->getQuestion(),
  233. ],
  234. ],
  235. ], JSON_THROW_ON_ERROR);
  236. $this->chatManager->addSystemMessage($this->room, $attendee->getActorType(), $attendee->getActorId(), $message, $this->timeFactory->getDateTime(), true);
  237. } catch (\Exception $e) {
  238. $this->logger->error($e->getMessage(), ['exception' => $e]);
  239. }
  240. $detailedVotes = [];
  241. if ($poll->getResultMode() === Poll::MODE_PUBLIC) {
  242. $detailedVotes = $this->pollService->getVotes($poll);
  243. }
  244. $votedSelf = $this->pollService->getVotesForActor($this->participant, $poll);
  245. return new DataResponse($this->renderPoll($poll, $votedSelf, $detailedVotes));
  246. }
  247. /**
  248. * @return TalkPoll
  249. * @throws JsonException
  250. */
  251. protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array {
  252. $data = $poll->asArray();
  253. $canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC;
  254. if (!$canSeeSummary && $poll->getStatus() === Poll::STATUS_OPEN) {
  255. $data['votes'] = [];
  256. if ($this->participant->hasModeratorPermissions()
  257. || ($poll->getActorType() === $this->participant->getAttendee()->getActorType()
  258. && $poll->getActorId() === $this->participant->getAttendee()->getActorId())) {
  259. // Allow moderators and the author to see the number of voters,
  260. // So they know when to close the poll.
  261. } else {
  262. $data['numVoters'] = 0;
  263. }
  264. } elseif ($poll->getResultMode() === Poll::MODE_PUBLIC && $poll->getStatus() === Poll::STATUS_CLOSED) {
  265. $data['details'] = array_map(static fn (Vote $vote) => $vote->asArray(), $detailedVotes);
  266. }
  267. $data['votedSelf'] = array_map(static fn (Vote $vote) => $vote->getOptionId(), $votedSelf);
  268. return $data;
  269. }
  270. }