diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index ee127238c8..387bf6dd0a 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -793,8 +793,6 @@ class ChatManager { $comment->setMetaData($metaData); $this->commentsManager->save($comment); - $this->participantService->resetHiddenPinnedId($chat, (int)$comment->getId()); - $this->roomService->setLastPinnedId($chat, (int)$comment->getId()); $this->attachmentService->createAttachmentEntryGeneric( diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 524cb85f56..ef2b658e14 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -1729,12 +1729,13 @@ class ChatController extends AEnvironmentAwareOCSController { * @psalm-param non-negative-int $messageId * @param int $pinUntil Unix timestamp when to unpin the message * @psalm-param non-negative-int $pinUntil - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse * * 200: Message was pinned successfully * 400: Message could not be pinned * 404: Message was not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorParticipant] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] @@ -1744,7 +1745,11 @@ class ChatController extends AEnvironmentAwareOCSController { 'messageId' => '[0-9]+', ])] public function pinMessage(int $messageId, int $pinUntil = 0): DataResponse { - // FIXME add federation + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class); + return $proxy->pinMessage($this->room, $this->participant, $messageId, $pinUntil); + } try { $comment = $this->chatManager->getComment($this->room, (string)$messageId); @@ -1774,11 +1779,13 @@ class ChatController extends AEnvironmentAwareOCSController { * * @param int $messageId ID of the message * @psalm-param non-negative-int $messageId - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Message is not pinned now + * 400: Federation request answered with an unknown status code * 404: Message was not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorParticipant] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] @@ -1788,7 +1795,11 @@ class ChatController extends AEnvironmentAwareOCSController { 'messageId' => '[0-9]+', ])] public function unpinMessage(int $messageId): DataResponse { - // FIXME add federation + if ($this->room->isFederatedConversation()) { + /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */ + $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class); + return $proxy->unpinMessage($this->room, $this->participant, $messageId); + } try { $comment = $this->chatManager->getComment($this->room, (string)$messageId); @@ -1813,6 +1824,7 @@ class ChatController extends AEnvironmentAwareOCSController { * 200: Pinned message is now hidden * 404: Message was not found */ + #[FederationSupported] #[PublicPage] #[RequireModeratorOrNoLobby] #[RequireParticipant] diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index ede725f34a..6733f5025f 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -22,6 +22,7 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public const PROPERTY_DEFAULT_PERMISSIONS = 'defaultPermissions'; public const PROPERTY_DESCRIPTION = 'description'; public const PROPERTY_IN_CALL = 'inCall'; + public const PROPERTY_LAST_PINNED_ID = 'lastPinnedId'; public const PROPERTY_LISTABLE = 'listable'; public const PROPERTY_LOBBY = 'lobby'; public const PROPERTY_LIVE_TRANSCRIPTION_LANGUAGE_ID = 'liveTranscriptionLanguageId'; diff --git a/lib/Federation/BackendNotifier.php b/lib/Federation/BackendNotifier.php index fed890758e..58fd61616b 100644 --- a/lib/Federation/BackendNotifier.php +++ b/lib/Federation/BackendNotifier.php @@ -532,6 +532,7 @@ class BackendNotifier { ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS => $room->getDefaultPermissions(), ARoomModifiedEvent::PROPERTY_DESCRIPTION => $room->getDescription(), ARoomModifiedEvent::PROPERTY_IN_CALL => $room->getCallFlag(), + ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID => $room->getLastPinnedId(), ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS => $room->getMentionPermissions(), ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION => $room->getMessageExpiration(), ARoomModifiedEvent::PROPERTY_NAME => $room->getName(), diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 111bc274b5..6397622036 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -408,6 +408,8 @@ class CloudFederationProviderTalk implements ICloudFederationProvider, ISignedCl } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS) { /** @psalm-suppress InvalidArgument */ $this->roomService->setMentionPermissions($room, $notification['newValue']); + } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID) { + $this->roomService->setLastPinnedId($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION) { $this->roomService->setMessageExpiration($room, $notification['newValue']); } elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_NAME) { diff --git a/lib/Federation/Proxy/TalkV1/Controller/ChatController.php b/lib/Federation/Proxy/TalkV1/Controller/ChatController.php index 79df4ca16c..fe5faa05a0 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/ChatController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/ChatController.php @@ -313,6 +313,85 @@ class ChatController { return new DataResponse($data, Http::STATUS_OK); } + /** + * @return DataResponse|DataResponse + * @throws CannotReachRemoteException + * + * 200: Message was pinned successfully + * 400: Message could not be pinned + * 404: Message was not found + * + * @see \OCA\Talk\Controller\ChatController::pinMessage() + */ + public function pinMessage(Room $room, Participant $participant, int $messageId, int $pinUntil): DataResponse { + $proxy = $this->proxy->post( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId . '/pin', + ['pinUntil' => $pinUntil] + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK) { + if ($statusCode !== Http::STATUS_NOT_FOUND) { + $statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + $data = ['error' => 'status']; + return new DataResponse($data, $statusCode); + } + /** @var array{error: 'message'|'until'|'status'} $data */ + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_NOT_FOUND, Http::STATUS_BAD_REQUEST]); + return new DataResponse($data, $statusCode); + } + + /** @var ?TalkChatMessageWithParent $data */ + $data = $this->proxy->getOCSData($proxy, default: null); + if (!empty($data)) { + /** @var TalkChatMessageWithParent $data */ + $data = $this->userConverter->convertMessage($room, $data); + } + + return new DataResponse($data, Http::STATUS_OK); + } + + /** + * @return DataResponse|DataResponse|DataResponse + * @throws CannotReachRemoteException + * + * 200: Message is not pinned now + * 400: Federation request answered with an unknown status code + * 404: Message was not found + * + * @see \OCA\Talk\Controller\ChatController::unpinMessage() + */ + public function unpinMessage(Room $room, Participant $participant, int $messageId): DataResponse { + $proxy = $this->proxy->delete( + $participant->getAttendee()->getInvitedCloudId(), + $participant->getAttendee()->getAccessToken(), + $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/chat/' . $room->getRemoteToken() . '/' . $messageId . '/pin', + ); + + $statusCode = $proxy->getStatusCode(); + if ($statusCode !== Http::STATUS_OK) { + if ($statusCode !== Http::STATUS_NOT_FOUND) { + $statusCode = $this->proxy->logUnexpectedStatusCode(__METHOD__, $statusCode); + $data = ['error' => 'status']; + return new DataResponse($data, $statusCode); + } + /** @var array{error: 'message'} $data */ + $data = $this->proxy->getOCSData($proxy, [Http::STATUS_NOT_FOUND]); + return new DataResponse($data, Http::STATUS_NOT_FOUND); + } + + /** @var ?TalkChatMessageWithParent $data */ + $data = $this->proxy->getOCSData($proxy, default: null); + if (!empty($data)) { + /** @var TalkChatMessageWithParent $data */ + $data = $this->userConverter->convertMessage($room, $data); + } + + return new DataResponse($data, Http::STATUS_OK); + } + /** * @return DataResponse|DataResponse|DataResponse * @throws CannotReachRemoteException diff --git a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php index bb14d1d6cc..909466a8ef 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/RoomModifiedListener.php @@ -54,6 +54,7 @@ class RoomModifiedListener implements IEventListener { ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS, ARoomModifiedEvent::PROPERTY_DESCRIPTION, ARoomModifiedEvent::PROPERTY_IN_CALL, + ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID, ARoomModifiedEvent::PROPERTY_LOBBY, ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS, ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION, diff --git a/lib/Federation/Proxy/TalkV1/ProxyRequest.php b/lib/Federation/Proxy/TalkV1/ProxyRequest.php index 5f26c1589c..76839945df 100644 --- a/lib/Federation/Proxy/TalkV1/ProxyRequest.php +++ b/lib/Federation/Proxy/TalkV1/ProxyRequest.php @@ -230,10 +230,13 @@ class ProxyRequest { } /** + * @template T of array|null * @param list $allowedStatusCodes + * @param T $default + * @return array|T * @throws CannotReachRemoteException */ - public function getOCSData(IResponse $response, array $allowedStatusCodes = [Http::STATUS_OK]): array { + public function getOCSData(IResponse $response, array $allowedStatusCodes = [Http::STATUS_OK], ?array $default = []): ?array { if (!in_array($response->getStatusCode(), $allowedStatusCodes, true)) { $this->logUnexpectedStatusCode(__METHOD__, $response->getStatusCode()); } @@ -249,6 +252,6 @@ class ProxyRequest { throw new CannotReachRemoteException('Error parsing JSON response', $e->getCode(), $e); } - return $responseData['ocs']['data'] ?? []; + return $responseData['ocs']['data'] ?? $default; } } diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 61f406f09e..b8032c47b4 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -883,6 +883,9 @@ class RoomService { } public function setLastPinnedId(Room $room, int $lastPinnedId): void { + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID, $lastPinnedId); + $this->dispatcher->dispatchTyped($event); + $update = $this->db->getQueryBuilder(); $update->update('talk_rooms') ->set('last_pinned_id', $update->createNamedParameter($lastPinnedId)) @@ -890,6 +893,10 @@ class RoomService { $update->executeStatement(); $room->setLastPinnedId($lastPinnedId); + $this->participantService->resetHiddenPinnedId($room, $lastPinnedId); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID, $lastPinnedId); + $this->dispatcher->dispatchTyped($event); } public function setAssignedSignalingServer(Room $room, ?int $signalingServer): bool { @@ -1362,6 +1369,10 @@ class RoomService { $this->logger->error('An error (' . $e->getReason() . ') occurred while trying to sync mentionPermissions of ' . $local->getId() . ' to ' . $host['mentionPermissions'], ['exception' => $e]); } } + if (isset($host['lastPinnedId']) && $host['lastPinnedId'] !== $local->getLastPinnedId()) { + $this->setLastPinnedId($local, $host['lastPinnedId']); + $changed[] = ARoomModifiedEvent::PROPERTY_LAST_PINNED_ID; + } if (isset($host['messageExpiration']) && $host['messageExpiration'] !== $local->getMessageExpiration()) { try { $this->setMessageExpiration($local, $host['messageExpiration']); diff --git a/tests/integration/features/federation/chat.feature b/tests/integration/features/federation/chat.feature index 564b61f574..b336031abc 100644 --- a/tests/integration/features/federation/chat.feature +++ b/tests/integration/features/federation/chat.feature @@ -556,3 +556,54 @@ Feature: federation/chat Then user "participant2" sees the following shared location in room "LOCAL::room" with 200 | room | actorType | actorId | actorDisplayName | message | messageParameters | | LOCAL::room | federated_users | participant1@{$LOCAL_URL} | participant1-displayname | {object} | "IGNORE" | + + Scenario: Pin handling as a federated user + 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 using server "REMOTE" + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@LOCAL | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | LOCAL::room | room | 2 | LOCAL | room | + Then user "participant2" is participant of the following rooms (v4) + | id | type | + | LOCAL::room | 2 | + + And using server "LOCAL" + When user "participant1" sends message "Message 1" to room "room" with 201 + When user "participant1" pins message "Message 1" in room "room" with 200 + And using server "REMOTE" + Then user "participant2" is participant of the following rooms (v4) + | id | type | lastPinnedId | hiddenPinnedId | + | LOCAL::room | 2 | Message 1 | EMPTY | + When user "participant2" hides pinned message "Message 1" in room "LOCAL::room" with 200 + Then user "participant2" is participant of the following rooms (v4) + | id | type | lastPinnedId | hiddenPinnedId | + | LOCAL::room | 2 | Message 1 | Message 1 | + + # Unpinning resets lastPinnedId + And using server "LOCAL" + When user "participant1" unpins message "Message 1" in room "room" with 200 + And using server "REMOTE" + Then user "participant2" is participant of the following rooms (v4) + | id | type | lastPinnedId | hiddenPinnedId | + | LOCAL::room | 2 | EMPTY | Message 1 | + + # Pin temporarily + And using server "LOCAL" + When user "participant1" pins message "Message 1" for 3 seconds in room "room" with 200 + And using server "REMOTE" + Then user "participant2" is participant of the following rooms (v4) + | id | type | lastPinnedId | hiddenPinnedId | + | LOCAL::room | 2 | Message 1 | EMPTY | + When wait for 4 seconds + And using server "LOCAL" + And run "OCA\Talk\BackgroundJob\UnpinMessage" background jobs + And using server "REMOTE" + Then user "participant2" is participant of the following rooms (v4) + | id | type | lastPinnedId | hiddenPinnedId | + | LOCAL::room | 2 | EMPTY | EMPTY |