Merge pull request #13883 from nextcloud/feat/edit-delete-poll-drafts

feat(polls): allow editing of draft polls
This commit is contained in:
Joas Schilling 2025-01-23 16:27:21 +01:00 committed by GitHub
commit 1bf16889a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1182 additions and 78 deletions

View file

@ -21,6 +21,8 @@ return [
'ocs' => [
/** @see \OCA\Talk\Controller\PollController::createPoll() */
['name' => 'Poll#createPoll', 'url' => '/api/{apiVersion}/poll/{token}', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\PollController::updateDraftPoll() */
['name' => 'Poll#updateDraftPoll', 'url' => '/api/{apiVersion}/poll/{token}/draft/{pollId}', 'verb' => 'POST', 'requirements' => $requirementsWithPollId],
/** @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() */

View file

@ -177,3 +177,4 @@
* `config => conversations => description-length` (local) - The maximum length for conversation descriptions, currently 2000. Before this config was added the implicit limit was 500, since the existance of the feature capability `room-description`.
* `call-end-to-end-encryption` - Signaling support of the server for the end-to-end encryption of calls
* `config => call => end-to-end-encryption` - Whether calls should be end-to-end encrypted (currently off by default, until all Talk mobile clients support it)
+ `edit-draft-poll` - Whether moderators can edit draft polls

View file

@ -30,6 +30,31 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
See [Poll data](#poll-data)
# Edit a draft poll in a conversation
* Required capability: `edit-draft-poll`
* Method: `POST`
* Endpoint: `/poll/{token}/draft/{pollId}`
* Data:
| field | type | Description |
|--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `question` | string | The question of the poll |
| `options` | string[] | Array of strings with the voting options |
| `resultMode` | int | The result and voting mode of the poll, `0` means participants can immediatelly see the result and who voted for which option. `1` means the result is hidden until the poll is closed and then only the summary is published. |
| `maxVotes` | int | Maximum amount of options a participant can vote for |
* Response:
- Status code:
+ `200 OK`
+ `400 Bad Request` Modifying poll is not possible
+ `403 Forbidden` No permission to modify this poll
+ `404 Not Found` When the draft poll could not be found
- Data:
See [Poll data](#poll-data)
## Get state or result of a poll
* Federation capability: `federation-v1`

View file

@ -113,6 +113,7 @@ class Capabilities implements IPublicCapability {
'conversation-creation-password',
'call-notification-state-api',
'schedule-meeting',
'edit-draft-poll',
];
public const CONDITIONAL_FEATURES = [

View file

@ -61,7 +61,7 @@ class PollController extends AEnvironmentAwareOCSController {
* @psalm-param Poll::MODE_* $resultMode Mode how the results will be shown
* @param int $maxVotes Number of maximum votes per voter
* @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{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
*
* 200: Draft created successfully
* 201: Poll created successfully
@ -133,6 +133,79 @@ class PollController extends AEnvironmentAwareOCSController {
return new DataResponse($this->renderPoll($poll), Http::STATUS_CREATED);
}
/**
* Modify a draft poll
*
* Required capability: `edit-draft-poll`
*
* @param int $pollId The poll id
* @param string $question Question of the poll
* @param string[] $options Options of the poll
* @psalm-param list<string> $options
* @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_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
*
* 200: Draft modified successfully
* 400: Modifying poll is not possible
* 403: No permission to modify this poll
* 404: No draft poll exists
*/
#[FederationSupported]
#[PublicPage]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[RequirePermission(permission: RequirePermission::CHAT)]
#[RequireReadWriteConversation]
public function updateDraftPoll(int $pollId, string $question, array $options, int $resultMode, int $maxVotes): 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->updateDraftPoll($pollId, $this->room, $this->participant, $question, $options, $resultMode, $maxVotes);
}
if ($this->room->getType() !== Room::TYPE_GROUP
&& $this->room->getType() !== Room::TYPE_PUBLIC) {
return new DataResponse(['error' => PollPropertyException::REASON_ROOM], Http::STATUS_BAD_REQUEST);
}
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException $e) {
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_NOT_FOUND);
}
if (!$poll->isDraft()) {
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST);
}
if (!$this->participant->hasModeratorPermissions()
&& ($poll->getActorType() !== $this->participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $this->participant->getAttendee()->getActorId())) {
return new DataResponse(['error' => PollPropertyException::REASON_DRAFT], Http::STATUS_BAD_REQUEST);
}
try {
$poll->setQuestion($question);
$poll->setOptions($options);
$poll->setResultMode($resultMode);
$poll->setMaxVotes($maxVotes);
} catch (PollPropertyException $e) {
$this->logger->error('Error modifying poll', ['exception' => $e]);
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
}
try {
$this->pollService->updatePoll($this->participant, $poll);
} catch (WrongPermissionsException $e) {
$this->logger->error('Error modifying poll', ['exception' => $e]);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_FORBIDDEN);
}
return new DataResponse($poll->renderAsDraft());
}
/**
* Get all drafted polls
*
@ -273,7 +346,7 @@ class PollController extends AEnvironmentAwareOCSController {
*
* @param int $pollId ID of the poll
* @psalm-param non-negative-int $pollId
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
*
* 200: Poll closed successfully
* 202: Poll draft was deleted successfully
@ -295,7 +368,7 @@ class PollController extends AEnvironmentAwareOCSController {
try {
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
} catch (DoesNotExistException) {
return new DataResponse(['error' => 'poll'], Http::STATUS_NOT_FOUND);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_NOT_FOUND);
}
if ($poll->getStatus() === Poll::STATUS_DRAFT) {
@ -304,15 +377,13 @@ class PollController extends AEnvironmentAwareOCSController {
}
if ($poll->getStatus() === Poll::STATUS_CLOSED) {
return new DataResponse(['error' => 'poll'], Http::STATUS_BAD_REQUEST);
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_BAD_REQUEST);
}
$poll->setStatus(Poll::STATUS_CLOSED);
try {
$this->pollService->updatePoll($this->participant, $poll);
} catch (WrongPermissionsException) {
return new DataResponse(['error' => 'poll'], Http::STATUS_FORBIDDEN);
$this->pollService->closePoll($this->participant, $poll);
} catch (WrongPermissionsException $e) {
return new DataResponse(['error' => PollPropertyException::REASON_POLL], Http::STATUS_FORBIDDEN);
}
$attendee = $this->participant->getAttendee();

View file

@ -10,6 +10,7 @@ namespace OCA\Talk\Exceptions;
class PollPropertyException extends \InvalidArgumentException {
public const REASON_DRAFT = 'draft';
public const REASON_POLL = 'poll';
public const REASON_QUESTION = 'question';
public const REASON_OPTIONS = 'options';
public const REASON_ROOM = 'room';

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
use OCA\Talk\Participant;
@ -17,6 +18,7 @@ use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Room;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use Psr\Log\LoggerInterface;
/**
* @psalm-import-type TalkPoll from ResponseDefinitions
@ -26,6 +28,7 @@ class PollController {
public function __construct(
protected ProxyRequest $proxy,
protected UserConverter $userConverter,
protected LoggerInterface $logger,
) {
}
@ -131,7 +134,7 @@ class PollController {
/**
* @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{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'draft'|'options'|'poll'|'question'|'room'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Draft created successfully
@ -171,7 +174,46 @@ class PollController {
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: string}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkPollDraft, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'draft'|'options'|'poll'|'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 updateDraftPoll(int $pollId, 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() . '/draft/' . $pollId,
[
'question' => $question,
'options' => $options,
'resultMode' => $resultMode,
'maxVotes' => $maxVotes
],
);
$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 TalkPollDraft $data */
$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);
}
/**
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_ACCEPTED, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'poll'}, array{}>
* @throws CannotReachRemoteException
*
* 200: Poll closed successfully
@ -199,7 +241,12 @@ class PollController {
}
/** @var array{error?: string} $data */
$data = $this->proxy->getOCSData($proxy);
return new DataResponse(['error' => $data['error'] ?? 'poll'], $statusCode);
if ($data['error'] !== PollPropertyException::REASON_POLL) {
$this->logger->error('Unhandled error in ' . __METHOD__ . ': ' . $data['error']);
}
return new DataResponse(['error' => PollPropertyException::REASON_POLL], $statusCode);
}
/** @var TalkPoll $data */

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OCA\Talk\Model;
use OCA\Talk\Exceptions\PollPropertyException;
use OCA\Talk\ResponseDefinitions;
use OCP\AppFramework\Db\Entity;
use OCP\DB\Types;
@ -18,10 +19,8 @@ use OCP\DB\Types;
* @method void setRoomId(int $roomId)
* @method int getRoomId()
* @psalm-method int<1, max> getRoomId()
* @method void setQuestion(string $question)
* @method string getQuestion()
* @psalm-method non-empty-string getQuestion()
* @method void setOptions(string $options)
* @method string getOptions()
* @method void setVotes(string $votes)
* @method string getVotes()
@ -121,4 +120,57 @@ class Poll extends Entity {
'maxVotes' => $this->getMaxVotes(),
];
}
public function isDraft(): bool {
return $this->getStatus() === self::STATUS_DRAFT;
}
/**
* @param array $options
* @return void
* @throws PollPropertyException
*/
public function setOptions(array $options): void {
try {
$jsonOptions = json_encode($options, JSON_THROW_ON_ERROR, 1);
} catch (\Exception) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$validOptions = [];
foreach ($options as $option) {
if (!is_string($option)) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$option = trim($option);
if ($option !== '') {
$validOptions[] = $option;
}
}
if (count($validOptions) < 2) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
if (strlen($jsonOptions) > 60_000) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$this->setter('options', [$jsonOptions]);
}
/**
* @param string $question
* @return void
* @throws PollPropertyException
*/
public function setQuestion(string $question): void {
$question = trim($question);
if ($question === '' || strlen($question) > 32_000) {
throw new PollPropertyException(PollPropertyException::REASON_QUESTION);
}
$this->setter('question', [$question]);
}
}

View file

@ -44,12 +44,13 @@ class PollMapper extends QBMapper {
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
public function getByPollId(int $pollId): Poll {
public function getPollByRoomIdAndPollId(int $roomId, int $pollId): Poll {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)));
->where($query->expr()->eq('id', $query->createNamedParameter($pollId, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT)));
return $this->findEntity($query);
}

View file

@ -33,51 +33,13 @@ class PollService {
* @throws PollPropertyException
*/
public function createPoll(int $roomId, string $actorType, string $actorId, string $displayName, string $question, array $options, int $resultMode, int $maxVotes, bool $draft): Poll {
$question = trim($question);
if ($question === '' || strlen($question) > 32_000) {
throw new PollPropertyException(PollPropertyException::REASON_QUESTION);
}
try {
json_encode($options, JSON_THROW_ON_ERROR, 1);
} catch (\Exception) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$validOptions = [];
foreach ($options as $option) {
if (!is_string($option)) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$option = trim($option);
if ($option !== '') {
$validOptions[] = $option;
}
}
if (count($validOptions) < 2) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
try {
$jsonOptions = json_encode($validOptions, JSON_THROW_ON_ERROR, 1);
} catch (\Exception) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
if (strlen($jsonOptions) > 60_000) {
throw new PollPropertyException(PollPropertyException::REASON_OPTIONS);
}
$poll = new Poll();
$poll->setRoomId($roomId);
$poll->setActorType($actorType);
$poll->setActorId($actorId);
$poll->setDisplayName($displayName);
$poll->setQuestion($question);
$poll->setOptions($jsonOptions);
$poll->setOptions($options);
$poll->setVotes(json_encode([]));
$poll->setResultMode($resultMode);
$poll->setMaxVotes($maxVotes);
@ -105,13 +67,7 @@ class PollService {
* @throws DoesNotExistException
*/
public function getPoll(int $roomId, int $pollId): Poll {
$poll = $this->pollMapper->getByPollId($pollId);
if ($poll->getRoomId() !== $roomId) {
throw new DoesNotExistException('Room id mismatch');
}
return $poll;
return $this->pollMapper->getPollByRoomIdAndPollId($roomId, $pollId);
}
/**
@ -121,12 +77,26 @@ class PollService {
*/
public function updatePoll(Participant $participant, Poll $poll): void {
if (!$participant->hasModeratorPermissions()
&& ($poll->getActorType() !== $participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $participant->getAttendee()->getActorId())) {
&& ($poll->getActorType() !== $participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $participant->getAttendee()->getActorId())) {
// Only moderators and the author of the poll can update it
throw new WrongPermissionsException();
}
$this->pollMapper->update($poll);
}
/**
* @throws WrongPermissionsException
*/
public function closePoll(Participant $participant, Poll $poll): void {
if (!$participant->hasModeratorPermissions()
&& ($poll->getActorType() !== $participant->getAttendee()->getActorType()
|| $poll->getActorId() !== $participant->getAttendee()->getActorId())) {
// Only moderators and the author of the poll can update it
throw new WrongPermissionsException();
}
$poll->setStatus(Poll::STATUS_CLOSED);
$this->pollMapper->update($poll);
}

View file

@ -9517,6 +9517,279 @@
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": {
"post": {
"operationId": "poll-update-draft-poll",
"summary": "Modify a draft poll",
"description": "Required capability: `edit-draft-poll`",
"tags": [
"poll"
],
"security": [
{},
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"question",
"options",
"resultMode",
"maxVotes"
],
"properties": {
"question": {
"type": "string",
"description": "Question of the poll"
},
"options": {
"type": "array",
"description": "Options of the poll",
"items": {
"type": "string"
}
},
"resultMode": {
"type": "integer",
"format": "int64",
"enum": [
0,
1
],
"description": "Mode how the results will be shown"
},
"maxVotes": {
"type": "integer",
"format": "int64",
"description": "Number of maximum votes per voter"
}
}
}
}
}
},
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v1"
],
"default": "v1"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "pollId",
"in": "path",
"description": "The poll id",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Draft modified successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/PollDraft"
}
}
}
}
}
}
}
},
"400": {
"description": "Modifying poll is not possible",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
},
"403": {
"description": "No permission to modify this poll",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "No draft poll exists",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
@ -10143,7 +10416,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
@ -10181,7 +10461,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
@ -10219,7 +10506,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}

View file

@ -9422,6 +9422,279 @@
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": {
"post": {
"operationId": "poll-update-draft-poll",
"summary": "Modify a draft poll",
"description": "Required capability: `edit-draft-poll`",
"tags": [
"poll"
],
"security": [
{},
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"question",
"options",
"resultMode",
"maxVotes"
],
"properties": {
"question": {
"type": "string",
"description": "Question of the poll"
},
"options": {
"type": "array",
"description": "Options of the poll",
"items": {
"type": "string"
}
},
"resultMode": {
"type": "integer",
"format": "int64",
"enum": [
0,
1
],
"description": "Mode how the results will be shown"
},
"maxVotes": {
"type": "integer",
"format": "int64",
"description": "Number of maximum votes per voter"
}
}
}
}
}
},
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v1"
],
"default": "v1"
}
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "pollId",
"in": "path",
"description": "The poll id",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Draft modified successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"$ref": "#/components/schemas/PollDraft"
}
}
}
}
}
}
}
},
"400": {
"description": "Modifying poll is not possible",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
},
"403": {
"description": "No permission to modify this poll",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
}
}
}
}
}
}
},
"404": {
"description": "No draft poll exists",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
@ -10048,7 +10321,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
@ -10086,7 +10366,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}
@ -10124,7 +10411,14 @@
],
"properties": {
"error": {
"type": "string"
"type": "string",
"enum": [
"draft",
"options",
"poll",
"question",
"room"
]
}
}
}

View file

@ -643,6 +643,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Modify a draft poll
* @description Required capability: `edit-draft-poll`
*/
post: operations["poll-update-draft-poll"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": {
parameters: {
query?: never;
@ -5521,7 +5541,111 @@ export interface operations {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "question" | "room";
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
};
};
"poll-update-draft-poll": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v1";
token: string;
/** @description The poll id */
pollId: number;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Question of the poll */
question: string;
/** @description Options of the poll */
options: string[];
/**
* Format: int64
* @description Mode how the results will be shown
* @enum {integer}
*/
resultMode: 0 | 1;
/**
* Format: int64
* @description Number of maximum votes per voter
*/
maxVotes: number;
};
};
};
responses: {
/** @description Draft modified successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["PollDraft"];
};
};
};
};
/** @description Modifying poll is not possible */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
/** @description No permission to modify this poll */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
/** @description No draft poll exists */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5767,7 +5891,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5783,7 +5908,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5799,7 +5925,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};

View file

@ -643,6 +643,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/draft/{pollId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Modify a draft poll
* @description Required capability: `edit-draft-poll`
*/
post: operations["poll-update-draft-poll"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/poll/{token}/drafts": {
parameters: {
query?: never;
@ -5005,7 +5025,111 @@ export interface operations {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "question" | "room";
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
};
};
"poll-update-draft-poll": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v1";
token: string;
/** @description The poll id */
pollId: number;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description Question of the poll */
question: string;
/** @description Options of the poll */
options: string[];
/**
* Format: int64
* @description Mode how the results will be shown
* @enum {integer}
*/
resultMode: 0 | 1;
/**
* Format: int64
* @description Number of maximum votes per voter
*/
maxVotes: number;
};
};
};
responses: {
/** @description Draft modified successfully */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["PollDraft"];
};
};
};
};
/** @description Modifying poll is not possible */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
/** @description No permission to modify this poll */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
};
};
/** @description No draft poll exists */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5251,7 +5375,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5267,7 +5392,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};
@ -5283,7 +5409,8 @@ export interface operations {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
error: string;
/** @enum {string} */
error: "draft" | "options" | "poll" | "question" | "room";
};
};
};

View file

@ -2525,6 +2525,49 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
/**
* @Then /^user "([^"]*)" updates a draft poll in room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*
* @param string $user
* @param string $identifier
* @param string $statusCode
* @param string $apiVersion
*/
public function updateDraftPoll(string $user, string $identifier, string $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void {
$data = $formData->getRowsHash();
$data['options'] = json_decode($data['options'], true);
if ($data['resultMode'] === 'public') {
$data['resultMode'] = 0;
} elseif ($data['resultMode'] === 'hidden') {
$data['resultMode'] = 1;
} else {
throw new \Exception('Invalid result mode');
}
if ($data['maxVotes'] === 'unlimited') {
$data['maxVotes'] = 0;
}
$this->setCurrentUser($user);
$result = preg_match('/POLL_ID\(([^)]+)\)/', $data['id'], $matches);
if ($result) {
$data['id'] = self::$questionToPollId[$matches[1]];
}
$this->sendRequest(
'POST', '/apps/spreed/api/' . $apiVersion . '/poll/' . self::$identifierToToken[$identifier] . '/draft/' . $data['id'],
$data
);
$this->assertStatusCode($this->response, $statusCode);
if ($statusCode !== '200') {
return;
}
$response = $this->getDataFromResponse($this->response);
if (isset($response['id'])) {
self::$questionToPollId[$data['question']] = $response['id'];
}
}
/**
* @Then /^user "([^"]*)" gets poll drafts for room "([^"]*)" with (\d+)(?: \((v1)\))?$/
*

View file

@ -863,3 +863,51 @@ Feature: chat-2/poll
| room | actorType | actorId | systemMessage | message | silent | messageParameters |
| room | users | participant1 | user_added | {actor} added you | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"user":{"type":"user","id":"participant2","name":"participant2-displayname"}} |
| room | users | participant1 | conversation_created | {actor} created the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} |
Scenario: Update a Draft Poll
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 200
| question | What is the question? |
| options | ["You","me"] |
| resultMode | public |
| maxVotes | unlimited |
| draft | 1 |
When user "participant1" gets poll drafts for room "room" with 200
| id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes |
| POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 |
Then user "participant1" updates a draft poll in room "room" with 200
| id | POLL_ID(What is the question?) |
| question | What is the question again? |
| options | ["You","her"] |
| resultMode | public |
| maxVotes | unlimited |
When user "participant1" gets poll drafts for room "room" with 200
| id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes |
| POLL_ID(What is the question?) | What is the question again? | ["You","her"] | users | participant1 | participant1-displayname | draft | public | 0 |
Scenario: Update a Draft Poll fails
Given user "participant1" creates room "room" (v4)
| roomType | 2 |
| roomName | room |
When user "participant1" adds user "participant2" to room "room" with 200 (v4)
When user "participant1" creates a poll in room "room" with 200
| question | What is the question? |
| options | ["You","me"] |
| resultMode | public |
| maxVotes | unlimited |
| draft | 1 |
When user "participant1" gets poll drafts for room "room" with 200
| id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes |
| POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 |
Then user "participant1" updates a draft poll in room "room" with 400
| id | POLL_ID(What is the question?) |
| question | What is the question again? |
| options | [""] |
| resultMode | public |
| maxVotes | unlimited |
When user "participant1" gets poll drafts for room "room" with 200
| id | question | options | actorType | actorId | actorDisplayName | status | resultMode | maxVotes |
| POLL_ID(What is the question?) | What is the question? | ["You","me"] | users | participant1 | participant1-displayname | draft | public | 0 |