Skip to content

Commit

Permalink
Merge pull request #13506 from nextcloud/feat/13439/drafts-for-polls
Browse files Browse the repository at this point in the history
feat(polls): Allow moderators to draft polls
  • Loading branch information
nickvergessen authored Oct 11, 2024
2 parents 81c8bfc + 7c427d2 commit 58c1611
Show file tree
Hide file tree
Showing 17 changed files with 1,203 additions and 133 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes/routesPollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
'ocs' => [
/** @see \OCA\Talk\Controller\PollController::createPoll() */
['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::getAllDraftPolls() */
['name' => 'Poll#getAllDraftPolls', 'url' => '/api/{apiVersion}/poll/{token}/drafts', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::showPoll() */
['name' => 'Poll#showPoll', 'url' => '/api/{apiVersion}/poll/{token}/{pollId}', 'verb' => 'GET', 'requirements' => $requirementsWithPollId],
/** @see \OCA\Talk\Controller\PollController::votePoll() */
Expand Down
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,5 @@

## 20.1
* `archived-conversations` (local) - Conversations can be marked as archived which will hide them from the conversation list by default
* `talk-polls-drafts` - Whether moderators can store and retrieve poll drafts
* `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class Capabilities implements IPublicCapability {
'mention-permissions',
'edit-messages-note-to-self',
'archived-conversations',
'talk-polls-drafts',
];

public const LOCAL_FEATURES = [
Expand Down
84 changes: 71 additions & 13 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

use JsonException;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\Exceptions\WrongPermissionsException;
use OCA\Talk\Middleware\Attribute\FederationSupported;
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant;
use OCA\Talk\Middleware\Attribute\RequireParticipant;
use OCA\Talk\Middleware\Attribute\RequirePermission;
use OCA\Talk\Middleware\Attribute\RequireReadWriteConversation;
Expand All @@ -34,6 +36,7 @@

/**
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class PollController extends AEnvironmentAwareController {

Expand All @@ -58,8 +61,10 @@ public function __construct(
* @param 0|1 $resultMode Mode how the results will be shown
* @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown
* @param int $maxVotes Number of maximum votes per voter
* @return DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, array{}>
* @param bool $draft Whether the poll should be saved as a draft (only allowed for moderators and with `talk-polls-drafts` capability)
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'question'|'room'}, array{}>
*
* 200: Draft created successfully
* 201: Poll created successfully
* 400: Creating poll is not possible
*/
Expand All @@ -69,16 +74,20 @@ public function __construct(
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequireReadWriteConversation]
public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
public function createPoll(string $question, array $options, int $resultMode, int $maxVotes, bool $draft = false): DataResponse {
if ($this->room->isFederatedConversation()) {
/** @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);
return $proxy->createPoll($this->room, $this->participant, $question, $options, $resultMode, $maxVotes, $draft);
}

if ($this->room->getType() !== Room::TYPE_GROUP
&& $this->room->getType() !== Room::TYPE_PUBLIC) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => PollPropertyException::REASON_ROOM], Http::STATUS_BAD_REQUEST);
}

if ($draft === true && !$this->participant->hasModeratorPermissions()) {
return new DataResponse(['error' => PollPropertyException::REASON_DRAFT], Http::STATUS_BAD_REQUEST);
}

$attendee = $this->participant->getAttendee();
Expand All @@ -91,11 +100,16 @@ public function createPoll(string $question, array $options, int $resultMode, in
$question,
$options,
$resultMode,
$maxVotes
$maxVotes,
$draft,
);
} catch (\Exception $e) {
} catch (PollPropertyException $e) {
$this->logger->error('Error creating poll', ['exception' => $e]);
return new DataResponse([], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
}

if ($draft) {
return new DataResponse($poll->renderAsDraft());
}

$message = json_encode([
Expand All @@ -117,7 +131,37 @@ public function createPoll(string $question, array $options, int $resultMode, in
$this->logger->error($e->getMessage(), ['exception' => $e]);
}

return new DataResponse($this->renderPoll($poll, []), Http::STATUS_CREATED);
return new DataResponse($this->renderPoll($poll), Http::STATUS_CREATED);
}

/**
* Get all drafted polls
*
* Required capability: `talk-polls-drafts`
*
* @return DataResponse<Http::STATUS_OK, list<TalkPollDraft>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
*
* 200: Poll returned
* 403: User is not a moderator
* 404: Poll not found
*/
#[FederationSupported]
#[PublicPage]
#[RequireModeratorParticipant]
public function getAllDraftPolls(): DataResponse {
if ($this->room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
return $proxy->getDraftsForRoom($this->room, $this->participant);
}

$polls = $this->pollService->getDraftsForRoom($this->room->getId());
$data = [];
foreach ($polls as $poll) {
$data[] = $poll->renderAsDraft();
}

return new DataResponse($data);
}

/**
Expand All @@ -143,7 +187,11 @@ public function showPoll(int $pollId): DataResponse {

try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException $e) {
} catch (DoesNotExistException) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

if ($poll->getStatus() === Poll::STATUS_DRAFT && !$this->participant->hasModeratorPermissions()) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

Expand Down Expand Up @@ -181,7 +229,11 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse {

try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (\Exception $e) {
} catch (DoesNotExistException) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

if ($poll->getStatus() === Poll::STATUS_DRAFT) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

Expand Down Expand Up @@ -222,9 +274,10 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse {
*
* @param int $pollId ID of the poll
* @psalm-param non-negative-int $pollId
* @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{}>
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED|Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Poll closed successfully
* 202: Poll draft was deleted successfully
* 400: Poll already closed
* 403: Missing permissions to close poll
* 404: Poll not found
Expand All @@ -242,10 +295,15 @@ public function closePoll(int $pollId): DataResponse {

try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (\Exception $e) {
} catch (DoesNotExistException) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

if ($poll->getStatus() === Poll::STATUS_DRAFT) {
$this->pollService->deleteByPollId($poll->getId());
return new DataResponse([], Http::STATUS_ACCEPTED);
}

if ($poll->getStatus() === Poll::STATUS_CLOSED) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
Expand Down Expand Up @@ -293,7 +351,7 @@ public function closePoll(int $pollId): DataResponse {
* @throws JsonException
*/
protected function renderPoll(Poll $poll, array $votedSelf = [], array $detailedVotes = []): array {
$data = $poll->asArray();
$data = $poll->renderAsPoll();

$canSeeSummary = !empty($votedSelf) && $poll->getResultMode() === Poll::MODE_PUBLIC;

Expand Down
32 changes: 32 additions & 0 deletions lib/Exceptions/PollPropertyException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Exceptions;

class PollPropertyException extends \InvalidArgumentException {
public const REASON_DRAFT = 'draft';
public const REASON_QUESTION = 'question';
public const REASON_OPTIONS = 'options';
public const REASON_ROOM = 'room';

/**
* @param self::REASON_* $reason
*/
public function __construct(
protected string $reason,
) {
parent::__construct($reason);
}

/**
* @return self::REASON_*
*/
public function getReason(): string {
return $this->reason;
}
}
50 changes: 45 additions & 5 deletions lib/Federation/Proxy/TalkV1/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

/**
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
*/
class PollController {
public function __construct(
Expand All @@ -28,6 +29,38 @@ public function __construct(
) {
}

/**
* @return DataResponse<Http::STATUS_OK, list<TalkPollDraft>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>
* @throws CannotReachRemoteException
*
* 200: Polls returned
* 404: Polls not found
*
* @see \OCA\Talk\Controller\PollController::showPoll()
*/
public function getDraftsForRoom(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->get(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/drafts',
);

$status = $proxy->getStatusCode();
if ($status === Http::STATUS_NOT_FOUND || $status === Http::STATUS_FORBIDDEN) {
return new DataResponse([], $status);
}

/** @var list<TalkPollDraft> $list */
$list = $this->proxy->getOCSData($proxy);

$data = [];
foreach ($list as $poll) {
$data[] = $this->userConverter->convertPoll($room, $poll);
}

return new DataResponse($data);
}

/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
* @throws CannotReachRemoteException
Expand Down Expand Up @@ -93,15 +126,16 @@ public function votePoll(Room $room, Participant $participant, int $pollId, arra


/**
* @return DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'question'|'room'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Draft created successfully
* 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 {
public function createPoll(Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes, bool $draft): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
Expand All @@ -111,17 +145,23 @@ public function createPoll(Room $room, Participant $participant, string $questio
'options' => $options,
'resultMode' => $resultMode,
'maxVotes' => $maxVotes,
'draft' => $draft,
],
);

if ($proxy->getStatusCode() === Http::STATUS_BAD_REQUEST) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
$status = $proxy->getStatusCode();
if ($status === Http::STATUS_BAD_REQUEST) {
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_BAD_REQUEST]);
return new DataResponse($data, Http::STATUS_BAD_REQUEST);
}

/** @var TalkPoll $data */
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED]);
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_OK, Http::STATUS_CREATED]);
$data = $this->userConverter->convertPoll($room, $data);

if ($status === Http::STATUS_OK) {
return new DataResponse($data);
}
return new DataResponse($data, Http::STATUS_CREATED);
}

Expand Down
8 changes: 6 additions & 2 deletions lib/Federation/Proxy/TalkV1/UserConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
/**
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkPollDraft from ResponseDefinitions
* @psalm-import-type TalkReaction from ResponseDefinitions
*/
class UserConverter {
Expand Down Expand Up @@ -137,9 +138,12 @@ public function convertMessages(Room $room, array $messages): array {
}

/**
* @template T of TalkPoll|TalkPollDraft
* @param Room $room
* @param TalkPoll $poll
* @return TalkPoll
* @param TalkPoll|TalkPollDraft $poll
* @psalm-param T $poll
* @return TalkPoll|TalkPollDraft
* @psalm-return T
*/
public function convertPoll(Room $room, array $poll): array {
$poll = $this->convertAttendee($room, $poll, 'actorType', 'actorId', 'actorDisplayName');
Expand Down
Loading

0 comments on commit 58c1611

Please sign in to comment.