summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2024-03-14 16:48:56 +0100
committerGitHub <noreply@github.com>2024-03-14 16:48:56 +0100
commit14d44eefdcd4bb875514bb4798e8ba82ef425aeb (patch)
tree24d05163f3b979a00de820e8b23219e90795fec6
parent380f456c146f0b312f728b7b43e41650f4a8e1e1 (diff)
parent13a5f6790e902aa47c0d6d789a9a6fdbb3908536 (diff)
Merge pull request #11653 from nextcloud/feat/11272/polls
feat(federation): Implement polls
-rw-r--r--docs/poll.md4
-rw-r--r--lib/Controller/PollController.php29
-rw-r--r--lib/Federation/Proxy/TalkV1/Controller/PollController.php181
-rw-r--r--lib/Federation/Proxy/TalkV1/UserConverter.php17
-rw-r--r--src/components/NewMessage/NewMessage.vue3
-rw-r--r--src/components/NewMessage/NewMessageAttachments.vue1
-rw-r--r--tests/integration/features/bootstrap/FeatureContext.php16
-rw-r--r--tests/integration/features/federation/poll.feature109
8 files changed, 358 insertions, 2 deletions
diff --git a/docs/poll.md b/docs/poll.md
index 8d41e105f..7ea130a3a 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 762529e08..ae94b66ae 100644
--- a/lib/Controller/PollController.php
+++ b/lib/Controller/PollController.php
@@ -30,6 +30,7 @@ namespace OCA\Talk\Controller;
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 @@ class PollController extends AEnvironmentAwareController {
* 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 @@ class PollController extends AEnvironmentAwareController {
* 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 @@ class PollController extends AEnvironmentAwareController {
* 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 @@ class PollController extends AEnvironmentAwareController {
* 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 000000000..442b12677
--- /dev/null
+++ b/lib/Federation/Proxy/TalkV1/Controller/PollController.php
@@ -0,0 +1,181 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2024 Joas Schilling <coding@schilljs.com>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, 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<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array<empty>, 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<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, 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<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{}>
+ * @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 44ad8dc24..a1af19fdb 100644
--- a/lib/Federation/Proxy/TalkV1/UserConverter.php
+++ b/lib/Federation/Proxy/TalkV1/UserConverter.php
@@ -34,6 +34,7 @@ use OCA\Talk\Service\ParticipantService;
/**
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
+ * @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkReaction from ResponseDefinitions
*/
class UserConverter {
@@ -154,6 +155,22 @@ class UserConverter {
/**
* @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
* @return TalkReaction[]
*/
diff --git a/src/components/NewMessage/NewMessage.vue b/src/components/NewMessage/NewMessage.vue
index 181cda13b..7c15dfcea 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 778e25fbf..654774b46 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">
<template #icon>
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index 5efd6bf8a..afc431ed9 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -2216,6 +2216,22 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$expected['status'] = 1;
}
+ if (str_ends_with($expected['actorId'], '@{$BASE_URL}')) {
+ $expected['actorId'] = str_replace('{$BASE_URL}', rtrim($this->baseUrl, '/'), $expected['actorId']);
+ }
+ if (str_ends_with($expected['actorId'], '@{$REMOTE_URL}')) {
+ $expected['actorId'] = str_replace('{$REMOTE_URL}', rtrim($this->baseRemoteUrl, '/'), $expected['actorId']);
+ }
+
+ if (isset($expected['details'])) {
+ if (str_contains($expected['details'], '@{$BASE_URL}')) {
+ $expected['details'] = str_replace('{$BASE_URL}', rtrim($this->baseUrl, '/'), $expected['details']);
+ }
+ if (str_contains($expected['details'], '@{$REMOTE_URL}')) {
+ $expected['details'] = str_replace('{$REMOTE_URL}', rtrim($this->baseRemoteUrl, '/'), $expected['details']);
+ }
+ }
+
if ($expected['votedSelf'] === 'not voted') {
$expected['votedSelf'] = [];
} else {
diff --git a/tests/integration/features/federation/poll.feature b/tests/integration/features/federation/poll.feature
new file mode 100644
index 000000000..fe2efdbef
--- /dev/null
+++ b/tests/integration/features/federation/poll.feature
@@ -0,0 +1,109 @@
+Feature: chat-2/poll
+ Background:
+ Given user "participant1" exists
+ Given user "participant2" exists
+
+ Scenario: Create a public poll without max votes limit
+ Given the following "spreed" app config is set
+ | federation_enabled | yes |
+ Given user "participant1" creates room "room" (v4)
+ | roomType | 2 |
+ | roomName | room |
+ And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4)
+ And user "participant2" has the following invitations (v1)
+ | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName |
+ | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname |
+ And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
+ | id | name | type | remoteServer | remoteToken |
+ | room | room | 2 | LOCAL | room |
+ Then user "participant2" is participant of the following rooms (v4)
+ | id | type |
+ | room | 2 |
+ When user "participant2" creates a poll in room "LOCAL::room" with 201
+ | question | What is the question? |
+ | options | ["Where are you?","How much is the fish?"] |
+ | resultMode | public |
+ | maxVotes | unlimited |
+ Then user "participant1" sees the following messages in room "room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | room | federated_users | participant2@{$REMOTE_URL} | participant2-displayname | {object} | {"actor":{"type":"user","id":"participant2","name":"participant2-displayname","server":"http:\/\/localhost:8180"},"object":{"type":"talk-poll","id":POLL_ID(What is the question?),"name":"What is the question?"}} |
+ Then user "participant2" sees poll "What is the question?" in room "LOCAL::room" with 200
+ | id | POLL_ID(What is the question?) |
+ | question | What is the question? |
+ | options | ["Where are you?","How much is the fish?"] |
+ | votes | [] |
+ | numVoters | 0 |
+ | resultMode | public |
+ | maxVotes | unlimited |
+ | actorType | users |
+ | actorId | participant2 |
+ | actorDisplayName | participant2-displayname |
+ | status | open |
+ | votedSelf | not voted |
+ Then user "participant1" votes for options "[1]" on poll "What is the question?" in room "room" with 200
+ | id | POLL_ID(What is the question?) |
+ | question | What is the question? |
+ | options | ["Where are you?","How much is the fish?"] |
+ | votes | {"option-1":1} |
+ | numVoters | 1 |
+ | resultMode | public |
+ | maxVotes | unlimited |
+ | actorType | federated_users |
+ | actorId | participant2@{$REMOTE_URL} |
+ | actorDisplayName | participant2-displayname |
+ | status | open |
+ | votedSelf | [1] |
+ Then user "participant1" sees poll "What is the question?" in room "room" with 200
+ | id | POLL_ID(What is the question?) |
+ | question | What is the question? |
+ | options | ["Where are you?","How much is the fish?"] |
+ | votes | {"option-1":1}