Skip to content

Commit

Permalink
feat(federation): Implement polls
Browse files Browse the repository at this point in the history
Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed Mar 14, 2024
1 parent 8b1f791 commit 13a5f67
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 2 deletions.
4 changes: 4 additions & 0 deletions docs/poll.md
Expand Up @@ -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:
Expand Down Expand Up @@ -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}`

Expand All @@ -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:
Expand All @@ -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}`

Expand Down
29 changes: 29 additions & 0 deletions lib/Controller/PollController.php
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -80,12 +81,19 @@ public function __construct(
* 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);
Expand Down Expand Up @@ -140,10 +148,17 @@ public function createPoll(string $question, array $options, int $resultMode, in
* 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) {
Expand Down Expand Up @@ -171,10 +186,17 @@ public function showPoll(int $pollId): DataResponse {
* 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) {
Expand Down Expand Up @@ -225,10 +247,17 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse {
* 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) {
Expand Down
181 changes: 181 additions & 0 deletions 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);
}
}
17 changes: 17 additions & 0 deletions lib/Federation/Proxy/TalkV1/UserConverter.php
Expand Up @@ -34,6 +34,7 @@

/**
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkPoll from ResponseDefinitions
* @psalm-import-type TalkReaction from ResponseDefinitions
*/
class UserConverter {
Expand Down Expand Up @@ -152,6 +153,22 @@ public function convertMessages(Room $room, array $messages): array {
);
}

/**
* @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
Expand Down
3 changes: 1 addition & 2 deletions src/components/NewMessage/NewMessage.vue
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions src/components/NewMessage/NewMessageAttachments.vue
Expand Up @@ -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>
Expand Down
16 changes: 16 additions & 0 deletions tests/integration/features/bootstrap/FeatureContext.php
Expand Up @@ -2216,6 +2216,22 @@ protected function preparePollExpectedData(array $expected): array {
$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 {
Expand Down

0 comments on commit 13a5f67

Please sign in to comment.