Skip to content

Commit 14d44ee

Browse files
Merge pull request #11653 from nextcloud/feat/11272/polls
feat(federation): Implement polls
2 parents 380f456 + 13a5f67 commit 14d44ee

File tree

8 files changed

+358
-2
lines changed

8 files changed

+358
-2
lines changed

docs/poll.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
44

55
## Create a poll in a conversation
66

7+
* Federation capability: `federation-v1`
78
* Method: `POST`
89
* Endpoint: `/poll/{token}`
910
* Data:
@@ -31,6 +32,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
3132

3233
## Get state or result of a poll
3334

35+
* Federation capability: `federation-v1`
3436
* Method: `GET`
3537
* Endpoint: `/poll/{token}/{pollId}`
3638

@@ -48,6 +50,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
4850

4951
## Vote on a poll
5052

53+
* Federation capability: `federation-v1`
5154
* Method: `POST`
5255
* Endpoint: `/poll/{token}/{pollId}`
5356
* Data:
@@ -72,6 +75,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
7275

7376
## Close a poll
7477

78+
* Federation capability: `federation-v1`
7579
* Method: `DELETE`
7680
* Endpoint: `/poll/{token}/{pollId}`
7781

lib/Controller/PollController.php

+29
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use JsonException;
3131
use OCA\Talk\Chat\ChatManager;
3232
use OCA\Talk\Exceptions\WrongPermissionsException;
33+
use OCA\Talk\Middleware\Attribute\FederationSupported;
3334
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
3435
use OCA\Talk\Middleware\Attribute\RequireParticipant;
3536
use OCA\Talk\Middleware\Attribute\RequirePermission;
@@ -80,12 +81,19 @@ public function __construct(
8081
* 201: Poll created successfully
8182
* 400: Creating poll is not possible
8283
*/
84+
#[FederationSupported]
8385
#[PublicPage]
8486
#[RequireModeratorOrNoLobby]
8587
#[RequireParticipant]
8688
#[RequirePermission(permission: RequirePermission::CHAT)]
8789
#[RequireReadWriteConversation]
8890
public function createPoll(string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
91+
if ($this->room->getRemoteServer() !== '') {
92+
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
93+
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
94+
return $proxy->createPoll($this->room, $this->participant, $question, $options, $resultMode, $maxVotes);
95+
}
96+
8997
if ($this->room->getType() !== Room::TYPE_GROUP
9098
&& $this->room->getType() !== Room::TYPE_PUBLIC) {
9199
return new DataResponse([], Http::STATUS_BAD_REQUEST);
@@ -140,10 +148,17 @@ public function createPoll(string $question, array $options, int $resultMode, in
140148
* 200: Poll returned
141149
* 404: Poll not found
142150
*/
151+
#[FederationSupported]
143152
#[PublicPage]
144153
#[RequireModeratorOrNoLobby]
145154
#[RequireParticipant]
146155
public function showPoll(int $pollId): DataResponse {
156+
if ($this->room->getRemoteServer() !== '') {
157+
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
158+
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
159+
return $proxy->showPoll($this->room, $this->participant, $pollId);
160+
}
161+
147162
try {
148163
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
149164
} catch (DoesNotExistException $e) {
@@ -171,10 +186,17 @@ public function showPoll(int $pollId): DataResponse {
171186
* 400: Voting is not possible
172187
* 404: Poll not found
173188
*/
189+
#[FederationSupported]
174190
#[PublicPage]
175191
#[RequireModeratorOrNoLobby]
176192
#[RequireParticipant]
177193
public function votePoll(int $pollId, array $optionIds = []): DataResponse {
194+
if ($this->room->getRemoteServer() !== '') {
195+
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
196+
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
197+
return $proxy->votePoll($this->room, $this->participant, $pollId, $optionIds);
198+
}
199+
178200
try {
179201
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
180202
} catch (\Exception $e) {
@@ -225,10 +247,17 @@ public function votePoll(int $pollId, array $optionIds = []): DataResponse {
225247
* 403: Missing permissions to close poll
226248
* 404: Poll not found
227249
*/
250+
#[FederationSupported]
228251
#[PublicPage]
229252
#[RequireModeratorOrNoLobby]
230253
#[RequireParticipant]
231254
public function closePoll(int $pollId): DataResponse {
255+
if ($this->room->getRemoteServer() !== '') {
256+
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController $proxy */
257+
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\PollController::class);
258+
return $proxy->closePoll($this->room, $this->participant, $pollId);
259+
}
260+
232261
try {
233262
$poll = $this->pollService->getPoll($this->room->getId(), $pollId);
234263
} catch (\Exception $e) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright Copyright (c) 2024 Joas Schilling <[email protected]>
7+
*
8+
* @author Joas Schilling <[email protected]>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*
25+
*/
26+
27+
namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;
28+
29+
use OCA\Talk\Exceptions\CannotReachRemoteException;
30+
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
31+
use OCA\Talk\Federation\Proxy\TalkV1\UserConverter;
32+
use OCA\Talk\Participant;
33+
use OCA\Talk\ResponseDefinitions;
34+
use OCA\Talk\Room;
35+
use OCP\AppFramework\Http;
36+
use OCP\AppFramework\Http\DataResponse;
37+
38+
/**
39+
* @psalm-import-type TalkPoll from ResponseDefinitions
40+
*/
41+
class PollController {
42+
public function __construct(
43+
protected ProxyRequest $proxy,
44+
protected UserConverter $userConverter,
45+
) {
46+
}
47+
48+
/**
49+
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
50+
* @throws CannotReachRemoteException
51+
*
52+
* 200: Poll returned
53+
* 404: Poll not found
54+
*
55+
* @see \OCA\Talk\Controller\PollController::showPoll()
56+
*/
57+
public function showPoll(Room $room, Participant $participant, int $pollId): DataResponse {
58+
$proxy = $this->proxy->get(
59+
$participant->getAttendee()->getInvitedCloudId(),
60+
$participant->getAttendee()->getAccessToken(),
61+
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
62+
);
63+
64+
if ($proxy->getStatusCode() === Http::STATUS_NOT_FOUND) {
65+
return new DataResponse([], Http::STATUS_NOT_FOUND);
66+
}
67+
68+
/** @var TalkPoll $data */
69+
$data = $this->proxy->getOCSData($proxy);
70+
$data = $this->userConverter->convertPoll($room, $data);
71+
72+
return new DataResponse($data);
73+
}
74+
75+
/**
76+
* @return DataResponse<Http::STATUS_OK, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array<empty>, array{}>
77+
* @throws CannotReachRemoteException
78+
*
79+
* 200: Voted successfully
80+
* 400: Voting is not possible
81+
* 404: Poll not found
82+
*
83+
* @see \OCA\Talk\Controller\PollController::votePoll()
84+
*/
85+
public function votePoll(Room $room, Participant $participant, int $pollId, array $optionIds): DataResponse {
86+
$proxy = $this->proxy->post(
87+
$participant->getAttendee()->getInvitedCloudId(),
88+
$participant->getAttendee()->getAccessToken(),
89+
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
90+
['optionIds' => $optionIds],
91+
);
92+
93+
$statusCode = $proxy->getStatusCode();
94+
if ($statusCode !== Http::STATUS_OK) {
95+
if (!in_array($statusCode, [
96+
Http::STATUS_BAD_REQUEST,
97+
Http::STATUS_NOT_FOUND,
98+
], true)) {
99+
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
100+
}
101+
return new DataResponse([], $statusCode);
102+
}
103+
104+
/** @var TalkPoll $data */
105+
$data = $this->proxy->getOCSData($proxy);
106+
$data = $this->userConverter->convertPoll($room, $data);
107+
108+
return new DataResponse($data);
109+
}
110+
111+
112+
/**
113+
* @return DataResponse<Http::STATUS_CREATED, TalkPoll, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array<empty>, array{}>
114+
* @throws CannotReachRemoteException
115+
*
116+
* 201: Poll created successfully
117+
* 400: Creating poll is not possible
118+
*
119+
* @see \OCA\Talk\Controller\PollController::createPoll()
120+
*/
121+
public function createPoll(Room $room, Participant $participant, string $question, array $options, int $resultMode, int $maxVotes): DataResponse {
122+
$proxy = $this->proxy->post(
123+
$participant->getAttendee()->getInvitedCloudId(),
124+
$participant->getAttendee()->getAccessToken(),
125+
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken(),
126+
[
127+
'question' => $question,
128+
'options' => $options,
129+
'resultMode' => $resultMode,
130+
'maxVotes' => $maxVotes,
131+
],
132+
);
133+
134+
if ($proxy->getStatusCode() === Http::STATUS_BAD_REQUEST) {
135+
return new DataResponse([], Http::STATUS_BAD_REQUEST);
136+
}
137+
138+
/** @var TalkPoll $data */
139+
$data = $this->proxy->getOCSData($proxy, [Http::STATUS_CREATED]);
140+
$data = $this->userConverter->convertPoll($room, $data);
141+
142+
return new DataResponse($data, Http::STATUS_CREATED);
143+
}
144+
145+
/**
146+
* @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{}>
147+
* @throws CannotReachRemoteException
148+
*
149+
* 200: Poll closed successfully
150+
* 400: Poll already closed
151+
* 403: Missing permissions to close poll
152+
* 404: Poll not found
153+
*
154+
* @see \OCA\Talk\Controller\PollController::closePoll()
155+
*/
156+
public function closePoll(Room $room, Participant $participant, int $pollId): DataResponse {
157+
$proxy = $this->proxy->delete(
158+
$participant->getAttendee()->getInvitedCloudId(),
159+
$participant->getAttendee()->getAccessToken(),
160+
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/poll/' . $room->getRemoteToken() . '/' . $pollId,
161+
);
162+
163+
$statusCode = $proxy->getStatusCode();
164+
if ($statusCode !== Http::STATUS_OK) {
165+
if (!in_array($statusCode, [
166+
Http::STATUS_BAD_REQUEST,
167+
Http::STATUS_FORBIDDEN,
168+
Http::STATUS_NOT_FOUND,
169+
], true)) {
170+
$statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode);
171+
}
172+
return new DataResponse([], $statusCode);
173+
}
174+
175+
/** @var TalkPoll $data */
176+
$data = $this->proxy->getOCSData($proxy);
177+
$data = $this->userConverter->convertPoll($room, $data);
178+
179+
return new DataResponse($data);
180+
}
181+
}

lib/Federation/Proxy/TalkV1/UserConverter.php

+17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
/**
3636
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
37+
* @psalm-import-type TalkPoll from ResponseDefinitions
3738
* @psalm-import-type TalkReaction from ResponseDefinitions
3839
*/
3940
class UserConverter {
@@ -152,6 +153,22 @@ public function convertMessages(Room $room, array $messages): array {
152153
);
153154
}
154155

156+
/**
157+
* @param Room $room
158+
* @param TalkPoll $poll
159+
* @return TalkPoll
160+
*/
161+
public function convertPoll(Room $room, array $poll): array {
162+
$poll = $this->convertAttendee($room, $poll, 'actorType', 'actorId', 'actorDisplayName');
163+
if (isset($poll['details'])) {
164+
$poll['details'] = array_map(
165+
fn (array $vote): array => $this->convertAttendee($room, $vote, 'actorType', 'actorId', 'actorDisplayName'),
166+
$poll['details']
167+
);
168+
}
169+
return $poll;
170+
}
171+
155172
/**
156173
* @param Room $room
157174
* @param TalkReaction[] $reactions

src/components/NewMessage/NewMessage.vue

+1-2
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,6 @@ export default {
420420
canCreatePoll() {
421421
return !this.isOneToOne && !this.noChatPermission
422422
&& this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF
423-
&& (!supportFederationV1 || !this.conversation.remoteServer)
424423
},
425424

426425
currentConversationIsJoined() {
@@ -459,7 +458,7 @@ export default {
459458
},
460459

461460
showAttachmentsMenu() {
462-
return this.canShareFiles && !this.broadcast && !this.upload && !this.messageToEdit
461+
return (this.canUploadFiles || this.canShareFiles || this.canCreatePoll) && !this.broadcast && !this.upload && !this.messageToEdit
463462
},
464463

465464
showAudioRecorder() {

src/components/NewMessage/NewMessageAttachments.vue

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
:container="container"
2727
:boundaries-element="boundariesElement"
2828
:disabled="disabled"
29+
:force-menu="true"
2930
:aria-label="t('spreed', 'Share files to the conversation')"
3031
:aria-haspopup="true">
3132
<template #icon>

tests/integration/features/bootstrap/FeatureContext.php

+16
Original file line numberDiff line numberDiff line change
@@ -2216,6 +2216,22 @@ protected function preparePollExpectedData(array $expected): array {
22162216
$expected['status'] = 1;
22172217
}
22182218

2219+
if (str_ends_with($expected['actorId'], '@{$BASE_URL}')) {
2220+
$expected['actorId'] = str_replace('{$BASE_URL}', rtrim($this->baseUrl, '/'), $expected['actorId']);
2221+
}
2222+
if (str_ends_with($expected['actorId'], '@{$REMOTE_URL}')) {
2223+
$expected['actorId'] = str_replace('{$REMOTE_URL}', rtrim($this->baseRemoteUrl, '/'), $expected['actorId']);
2224+
}
2225+
2226+
if (isset($expected['details'])) {
2227+
if (str_contains($expected['details'], '@{$BASE_URL}')) {
2228+
$expected['details'] = str_replace('{$BASE_URL}', rtrim($this->baseUrl, '/'), $expected['details']);
2229+
}
2230+
if (str_contains($expected['details'], '@{$REMOTE_URL}')) {
2231+
$expected['details'] = str_replace('{$REMOTE_URL}', rtrim($this->baseRemoteUrl, '/'), $expected['details']);
2232+
}
2233+
}
2234+
22192235
if ($expected['votedSelf'] === 'not voted') {
22202236
$expected['votedSelf'] = [];
22212237
} else {

0 commit comments

Comments
 (0)