diff --git a/docs/capabilities.md b/docs/capabilities.md index cae4f691aa3..5a3ba6ab471 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -227,3 +227,6 @@ * `private-reply` - Whether clients can link the original message to a private reply in one-to-one conversations * `config => attachments => conversation-subfolders` (local) - Whether per-conversation subfolders are used for Talk attachments; when `true` files must be uploaded to `Talk/-/-/` before calling the attachment endpoint * `conversation-tags` (local) - Whether the user can create custom tags to organize conversations in the sidebar + +## 25 +* `preserve-conversation` - Whether the owner can preserve a conversation diff --git a/docs/constants.md b/docs/constants.md index 50c26d6d7f3..b39b1735df6 100644 --- a/docs/constants.md +++ b/docs/constants.md @@ -76,6 +76,7 @@ Required capability: `conversation-presets` * `0` None * `1` Voice rooms - Join call when joining conversation +* `2` Preserved - Conversation can not be deleted, its chat history can not be cleared and the guests (public link) and joinable (listable) settings can not be changed (only owners can toggle this attribute, requires capability `preserve-conversation`) ## Participants diff --git a/docs/occ.md b/docs/occ.md index c345dd6fc81..16cc4d36294 100644 --- a/docs/occ.md +++ b/docs/occ.md @@ -294,7 +294,7 @@ Create a new room ### Usage -* `talk:room:create [--description DESCRIPTION] [--user USER] [--group GROUP] [--public] [--readonly] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--moderator MODERATOR] [--message-expiration MESSAGE-EXPIRATION] [--] ` +* `talk:room:create [--description DESCRIPTION] [--user USER] [--group GROUP] [--public] [--readonly] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--moderator MODERATOR] [--message-expiration MESSAGE-EXPIRATION] [--preserve] [--] ` | Arguments | Description | Is required | Is array | Default | |---|---|---|---|---| @@ -312,6 +312,7 @@ Create a new room | `--owner` | Sets the given user as owner of the room to create | yes | yes | no | *Required* | | `--moderator` | Promotes the given users to moderators | yes | yes | yes | *Required* | | `--message-expiration` | Seconds to expire a message after sent. If zero will disable the expire message duration. | yes | yes | no | *Required* | +| `--preserve` | Preserves the room so it can not be deleted, its history cleared or its guests/joinable settings changed | no | no | no | `false` | ## talk:room:delete @@ -370,7 +371,7 @@ Updates a room ### Usage -* `talk:room:update [--name NAME] [--description DESCRIPTION] [--public PUBLIC] [--readonly READONLY] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--message-expiration MESSAGE-EXPIRATION] [--] ` +* `talk:room:update [--name NAME] [--description DESCRIPTION] [--public PUBLIC] [--readonly READONLY] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--message-expiration MESSAGE-EXPIRATION] [--preserve PRESERVE] [--] ` | Arguments | Description | Is required | Is array | Default | |---|---|---|---|---| @@ -386,6 +387,7 @@ Updates a room | `--password` | Sets a new password for the room; pass an empty value to remove password protection | yes | yes | no | *Required* | | `--owner` | Sets the given user as owner of the room; pass an empty value to remove the owner | yes | yes | no | *Required* | | `--message-expiration` | Seconds to expire a message after sent. If zero will disable the expire message duration. | yes | yes | no | *Required* | +| `--preserve` | Preserves the room (value 1) so it can not be deleted, its history cleared or its guests/joinable settings changed; pass value 0 to remove the protection | yes | yes | no | *Required* | ## talk:signaling:add diff --git a/lib/Capabilities.php b/lib/Capabilities.php index a3f3cbbf2e5..ab9e2eebef6 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -133,6 +133,7 @@ class Capabilities implements IPublicCapability { 'conversation-presets', 'private-reply', 'conversation-tags', + 'preserve-conversation', ]; public const CONDITIONAL_FEATURES = [ diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index 67ff026f2d2..6311c0eeafb 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -280,6 +280,20 @@ protected function parseMessage(Message $chatMessage, $allowInaccurate): void { } elseif ($cliIsActor) { $parsedMessage = $this->l->t('An administrator opened the conversation to registered users and users created with the Guests app'); } + } elseif ($message === 'preserve_conversation') { + $parsedMessage = $this->l->t('{actor} preserved the conversation'); + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You preserved the conversation'); + } elseif ($cliIsActor) { + $parsedMessage = $this->l->t('An administrator preserved the conversation'); + } + } elseif ($message === 'preserve_conversation_off') { + $parsedMessage = $this->l->t('{actor} stopped preserving the conversation'); + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You stopped preserving the conversation'); + } elseif ($cliIsActor) { + $parsedMessage = $this->l->t('An administrator stopped preserving the conversation'); + } } elseif ($message === 'lobby_timer_reached') { $parsedMessage = $this->l->t('The conversation is now open to everyone'); } elseif ($message === 'lobby_none') { diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 79ea5d4e0c4..1bf58b4c99d 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -100,6 +100,7 @@ public function handle(Event $event): void { ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION => $this->afterSetMessageExpiration($event), ARoomModifiedEvent::PROPERTY_NAME => $this->sendSystemMessageAboutConversationRenamed($event), ARoomModifiedEvent::PROPERTY_PASSWORD => $this->sendSystemMessageAboutRoomPassword($event), + ARoomModifiedEvent::PROPERTY_PRESERVE_CONVERSATION => $this->sendSystemPreserveConversationMessage($event), ARoomModifiedEvent::PROPERTY_READ_ONLY => $this->sendSystemReadOnlyMessage($event), ARoomModifiedEvent::PROPERTY_TYPE => $this->sendSystemGuestPermissionsMessage($event), default => null, @@ -268,6 +269,18 @@ protected function sendSystemListableMessage(RoomModifiedEvent $event): void { } } + protected function sendSystemPreserveConversationMessage(RoomModifiedEvent $event): void { + if ($event->getNewValue() === $event->getOldValue()) { + return; + } + + if ($event->getNewValue()) { + $this->sendSystemMessage($event->getRoom(), 'preserve_conversation'); + } else { + $this->sendSystemMessage($event->getRoom(), 'preserve_conversation_off'); + } + } + protected function sendSystemLobbyMessage(LobbyModifiedEvent $event): void { if ($event->getNewValue() === $event->getOldValue()) { return; diff --git a/lib/Command/Room/Create.php b/lib/Command/Room/Create.php index 66e7fd8b51f..0bc9a9bbce8 100644 --- a/lib/Command/Room/Create.php +++ b/lib/Command/Room/Create.php @@ -79,6 +79,11 @@ protected function configure(): void { null, InputOption::VALUE_REQUIRED, 'Seconds to expire a message after sent. If zero will disable the expire message duration.' + )->addOption( + 'preserve', + null, + InputOption::VALUE_NONE, + 'Preserves the room so it can not be deleted, its history cleared or its guests/joinable settings changed' ); } @@ -94,6 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $owner = $input->getOption('owner'); $moderators = $input->getOption('moderator'); $messageExpiration = $input->getOption('message-expiration'); + $preserve = $input->getOption('preserve'); if (!in_array($listable, [ null, @@ -139,6 +145,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($messageExpiration !== null) { $this->setMessageExpiration($room, (int)$messageExpiration); } + + if ($preserve) { + $this->setRoomPreserve($room, true); + } } catch (InvalidArgumentException $e) { $this->roomService->deleteRoom($room); diff --git a/lib/Command/Room/TRoomCommand.php b/lib/Command/Room/TRoomCommand.php index 8fcde102a77..5c112bd6976 100644 --- a/lib/Command/Room/TRoomCommand.php +++ b/lib/Command/Room/TRoomCommand.php @@ -149,6 +149,10 @@ protected function setRoomListable(Room $room, int $listable): void { } } + protected function setRoomPreserve(Room $room, bool $preserve): void { + $this->roomService->setPreserveConversation($room, $preserve); + } + /** * @param Room $room * @param string $password diff --git a/lib/Command/Room/Update.php b/lib/Command/Room/Update.php index e562cd12930..d4847b95d2f 100644 --- a/lib/Command/Room/Update.php +++ b/lib/Command/Room/Update.php @@ -70,6 +70,11 @@ protected function configure(): void { null, InputOption::VALUE_REQUIRED, 'Seconds to expire a message after sent. If zero will disable the expire message duration.' + )->addOption( + 'preserve', + null, + InputOption::VALUE_REQUIRED, + 'Preserves the room (value 1) so it can not be deleted, its history cleared or its guests/joinable settings changed; pass value 0 to remove the protection' ); } @@ -83,12 +88,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $password = $input->getOption('password'); $owner = $input->getOption('owner'); $messageExpiration = $input->getOption('message-expiration'); + $preserve = $input->getOption('preserve'); if (!in_array($public, [null, '0', '1'], true)) { $output->writeln('Invalid value for option "--public" given.'); return 1; } + if (!in_array($preserve, [null, '0', '1'], true)) { + $output->writeln('Invalid value for option "--preserve" given.'); + return 1; + } + if (!in_array($readOnly, [null, (string)Room::READ_WRITE, (string)Room::READ_ONLY], true)) { $output->writeln('Invalid value for option "--readonly" given.'); return 1; @@ -157,6 +168,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($messageExpiration !== null) { $this->setMessageExpiration($room, (int)$messageExpiration); } + + if ($preserve !== null) { + $this->setRoomPreserve($room, ($preserve === '1')); + } } catch (InvalidArgumentException $e) { $output->writeln(sprintf('%s', $e->getMessage())); return 1; diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 8784d8f722a..8fbf0ae7083 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -1842,6 +1842,11 @@ public function clearHistory(): DataResponse { return new DataResponse(null, Http::STATUS_FORBIDDEN); } + if ($this->room->isPreserved()) { + // Not allowed to purge a preserved conversation + return new DataResponse(null, Http::STATUS_FORBIDDEN); + } + if (!$this->appConfig->getAppValueBool('delete_one_to_one_conversations') && ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER)) { diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 0d8a2c56ac1..3a808e59a21 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1026,10 +1026,11 @@ public function setDescription(string $description): DataResponse { /** * Delete a room * - * @return DataResponse + * @return DataResponse|DataResponse * * 200: Room successfully deleted * 400: Deleting room is not possible + * 403: Conversation is preserved */ #[PublicPage] #[RequireModeratorParticipant] @@ -1038,6 +1039,10 @@ public function setDescription(string $description): DataResponse { 'token' => '[a-z0-9]{4,30}', ])] public function deleteRoom(): DataResponse { + if ($this->room->isPreserved()) { + return new DataResponse(['error' => 'preserved'], Http::STATUS_FORBIDDEN); + } + if (!$this->appConfig->getAppValueBool('delete_one_to_one_conversations') && in_array($this->room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) { return new DataResponse(null, Http::STATUS_BAD_REQUEST); @@ -1675,10 +1680,11 @@ public function removeAttendeeFromRoom(int $attendeeId): DataResponse { * Required capability: `conversation-creation-password` for `string $password` parameter * * @param string $password New password (only available with `conversation-creation-password` capability) - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Allowed guests successfully * 400: Allowing guests is not possible + * 403: Conversation is preserved */ #[NoAdminRequired] #[RequireLoggedInModeratorParticipant] @@ -1687,6 +1693,10 @@ public function removeAttendeeFromRoom(int $attendeeId): DataResponse { 'token' => '[a-z0-9]{4,30}', ])] public function makePublic(string $password = ''): DataResponse { + if ($this->room->isPreserved()) { + return new DataResponse(['error' => 'preserved'], Http::STATUS_FORBIDDEN); + } + if ($this->talkConfig->isPasswordEnforced() && $password === '') { return new DataResponse(['error' => 'password', 'message' => $this->l->t('Password needs to be set')], Http::STATUS_BAD_REQUEST); } @@ -1709,10 +1719,11 @@ public function makePublic(string $password = ''): DataResponse { /** * Disallowed guests to join conversation * - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Room unpublished Disallowing guests successfully * 400: Disallowing guests is not possible + * 403: Conversation is preserved */ #[NoAdminRequired] #[RequireLoggedInModeratorParticipant] @@ -1721,6 +1732,10 @@ public function makePublic(string $password = ''): DataResponse { 'token' => '[a-z0-9]{4,30}', ])] public function makePrivate(): DataResponse { + if ($this->room->isPreserved()) { + return new DataResponse(['error' => 'preserved'], Http::STATUS_FORBIDDEN); + } + try { $this->roomService->setType($this->room, Room::TYPE_GROUP); } catch (TypeException $e) { @@ -1770,10 +1785,11 @@ public function setReadOnly(int $state): DataResponse { * * @param 0|1|2 $scope Scope where the room is listable * @psalm-param Room::LISTABLE_* $scope - * @return DataResponse|DataResponse + * @return DataResponse|DataResponse|DataResponse * * 200: Made room listable successfully * 400: Making room listable is not possible + * 403: Conversation is preserved */ #[NoAdminRequired] #[RequireModeratorParticipant] @@ -1782,6 +1798,10 @@ public function setReadOnly(int $state): DataResponse { 'token' => '[a-z0-9]{4,30}', ])] public function setListable(int $scope): DataResponse { + if ($this->room->isPreserved()) { + return new DataResponse(['error' => 'preserved'], Http::STATUS_FORBIDDEN); + } + /** @var Room::LISTABLE_* $forced */ $forced = $this->forcedParameters->getForcedParameter(Parameter::LISTABLE); if ($forced !== null && $forced !== $scope) { @@ -1900,6 +1920,60 @@ public function unarchiveConversation(): DataResponse { return new DataResponse($this->formatRoom($this->room, $this->participant)); } + /** + * Preserve a conversation + * + * While preserved the conversation can not be deleted, its chat history can + * not be cleared and the guests (public link) and joinable (listable) + * settings can not be changed. + * + * Required capability: `preserve-conversation` + * + * @return DataResponse|DataResponse + * + * 200: Conversation was preserved + * 403: Only the owner can preserve a conversation + */ + #[NoAdminRequired] + #[RequireLoggedInModeratorParticipant] + #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/preserve', requirements: [ + 'apiVersion' => '(v4)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function preserveConversation(): DataResponse { + if ($this->participant->getAttendee()->getParticipantType() !== Participant::OWNER) { + return new DataResponse(['error' => 'permissions'], Http::STATUS_FORBIDDEN); + } + + $this->roomService->setPreserveConversation($this->room, true); + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } + + /** + * Stop preserving a conversation + * + * Required capability: `preserve-conversation` + * + * @return DataResponse|DataResponse + * + * 200: Conversation is not preserved anymore + * 403: Only the owner can stop preserving a conversation + */ + #[NoAdminRequired] + #[RequireLoggedInModeratorParticipant] + #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/room/{token}/preserve', requirements: [ + 'apiVersion' => '(v4)', + 'token' => '[a-z0-9]{4,30}', + ])] + public function unpreserveConversation(): DataResponse { + if ($this->participant->getAttendee()->getParticipantType() !== Participant::OWNER) { + return new DataResponse(['error' => 'permissions'], Http::STATUS_FORBIDDEN); + } + + $this->roomService->setPreserveConversation($this->room, false); + return new DataResponse($this->formatRoom($this->room, $this->participant)); + } + /** * Assign conversation tags * diff --git a/lib/Events/ARoomModifiedEvent.php b/lib/Events/ARoomModifiedEvent.php index fb7714d27cf..880b4b93060 100644 --- a/lib/Events/ARoomModifiedEvent.php +++ b/lib/Events/ARoomModifiedEvent.php @@ -28,6 +28,7 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public const PROPERTY_MENTION_PERMISSIONS = 'mentionPermissions'; public const PROPERTY_NAME = 'name'; public const PROPERTY_PASSWORD = 'password'; + public const PROPERTY_PRESERVE_CONVERSATION = 'preserveConversation'; public const PROPERTY_READ_ONLY = 'readOnly'; public const PROPERTY_RECORDING_CONSENT = 'recordingConsent'; public const PROPERTY_SIP_ENABLED = 'sipEnabled'; @@ -39,8 +40,8 @@ abstract class ARoomModifiedEvent extends ARoomEvent { public function __construct( Room $room, protected string $property, - protected \DateTime|string|int|null $newValue, - protected \DateTime|string|int|null $oldValue = null, + protected \DateTime|string|int|bool|null $newValue, + protected \DateTime|string|int|bool|null $oldValue = null, protected ?Participant $actor = null, ) { parent::__construct($room); @@ -50,11 +51,11 @@ public function getProperty(): string { return $this->property; } - public function getNewValue(): \DateTime|string|int|null { + public function getNewValue(): \DateTime|string|int|bool|null { return $this->newValue; } - public function getOldValue(): \DateTime|string|int|null { + public function getOldValue(): \DateTime|string|int|bool|null { return $this->oldValue; } diff --git a/lib/Room.php b/lib/Room.php index 503688979da..c8bf2f6232b 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -484,4 +484,8 @@ public function getAttributes(): int { public function setAttributes(int $attributes): void { $this->attributes = $attributes; } + + public function isPreserved(): bool { + return ($this->attributes & RoomAttributes::PRESERVE_CONVERSATION->value) === RoomAttributes::PRESERVE_CONVERSATION->value; + } } diff --git a/lib/RoomAttributes.php b/lib/RoomAttributes.php index c9520af816f..d9defa63a3a 100644 --- a/lib/RoomAttributes.php +++ b/lib/RoomAttributes.php @@ -11,4 +11,5 @@ enum RoomAttributes: int { case NONE = 0; case VOICE_ROOM = 1; + case PRESERVE_CONVERSATION = 2; } diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index dc7b17768d2..a7a13b0a5ed 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -338,6 +338,7 @@ public function formatRoomV4( $roomData['canDeleteConversation'] = $room->getType() !== Room::TYPE_ONE_TO_ONE && $room->getType() !== Room::TYPE_ONE_TO_ONE_FORMER + && !$room->isPreserved() && $currentParticipant->hasModeratorPermissions(false); $roomData['canLeaveConversation'] = $room->getType() !== Room::TYPE_NOTE_TO_SELF; diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 705857d2918..30d7d7c4281 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -837,6 +837,41 @@ public function setListable(Room $room, int $newState): void { $this->dispatcher->dispatchTyped($event); } + /** + * Mark a conversation as preserved (or remove the mark again). + * + * While preserved the conversation can not be deleted, its chat history can + * not be cleared and the "guests" (public link) and "joinable" (listable) + * settings can not be changed via the API. + */ + public function setPreserveConversation(Room $room, bool $preserve): void { + $oldPreserve = $room->isPreserved(); + if ($preserve === $oldPreserve) { + return; + } + + $oldAttributes = $room->getAttributes(); + if ($preserve) { + $newAttributes = $oldAttributes | RoomAttributes::PRESERVE_CONVERSATION->value; + } else { + $newAttributes = $oldAttributes & ~RoomAttributes::PRESERVE_CONVERSATION->value; + } + + $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PRESERVE_CONVERSATION, $preserve, $oldPreserve); + $this->dispatcher->dispatchTyped($event); + + $update = $this->db->getQueryBuilder(); + $update->update('talk_rooms') + ->set('attributes', $update->createNamedParameter($newAttributes, IQueryBuilder::PARAM_INT)) + ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))); + $update->executeStatement(); + + $room->setAttributes($newAttributes); + + $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PRESERVE_CONVERSATION, $preserve, $oldPreserve); + $this->dispatcher->dispatchTyped($event); + } + /** * @param Room $room * @param int $newState New mention permissions from Room::MENTION_PERMISSIONS_* diff --git a/openapi-full.json b/openapi-full.json index 3cddb5c40ab..168505b024d 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -19888,6 +19888,47 @@ } } } + }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } } } } @@ -22013,6 +22054,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -22164,6 +22246,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -22545,6 +22668,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -23147,6 +23311,308 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/preserve": { + "post": { + "operationId": "room-preserve-conversation", + "summary": "Preserve a conversation", + "description": "While preserved the conversation can not be deleted, its chat history can not be cleared and the guests (public link) and joinable (listable) settings can not be changed.\nRequired capability: `preserve-conversation`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "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": "Conversation was preserved", + "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/Room" + } + } + } + } + } + } + } + }, + "403": { + "description": "Only the owner can preserve a conversation", + "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": [ + "permissions" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "room-unpreserve-conversation", + "summary": "Stop preserving a conversation", + "description": "Required capability: `preserve-conversation`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "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": "Conversation is not preserved anymore", + "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/Room" + } + } + } + } + } + } + } + }, + "403": { + "description": "Only the owner can stop preserving a conversation", + "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": [ + "permissions" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/tags": { "post": { "operationId": "room-assign-tags", diff --git a/openapi.json b/openapi.json index 88a70a97ff4..bec682420ad 100644 --- a/openapi.json +++ b/openapi.json @@ -19776,6 +19776,47 @@ } } } + }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } } } } @@ -21901,6 +21942,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -22052,6 +22134,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -22433,6 +22556,47 @@ } } }, + "403": { + "description": "Conversation is preserved", + "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": [ + "preserved" + ] + } + } + } + } + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -23035,6 +23199,308 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/preserve": { + "post": { + "operationId": "room-preserve-conversation", + "summary": "Preserve a conversation", + "description": "While preserved the conversation can not be deleted, its chat history can not be cleared and the guests (public link) and joinable (listable) settings can not be changed.\nRequired capability: `preserve-conversation`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "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": "Conversation was preserved", + "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/Room" + } + } + } + } + } + } + } + }, + "403": { + "description": "Only the owner can preserve a conversation", + "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": [ + "permissions" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "room-unpreserve-conversation", + "summary": "Stop preserving a conversation", + "description": "Required capability: `preserve-conversation`", + "tags": [ + "room" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "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": "Conversation is not preserved anymore", + "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/Room" + } + } + } + } + } + } + } + }, + "403": { + "description": "Only the owner can stop preserving a conversation", + "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": [ + "permissions" + ] + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/tags": { "post": { "operationId": "room-assign-tags", diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 0fd412611f7..b92fb66fec9 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -103,9 +103,10 @@ +