Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ConversationName>-<token>/<DisplayName>-<uid>/` 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
1 change: 1 addition & 0 deletions docs/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions docs/occ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] [--] <name>`
* `talk:room:create [--description DESCRIPTION] [--user USER] [--group GROUP] [--public] [--readonly] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--moderator MODERATOR] [--message-expiration MESSAGE-EXPIRATION] [--preserve] [--] <name>`

| Arguments | Description | Is required | Is array | Default |
|---|---|---|---|---|
Expand All @@ -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

Expand Down Expand Up @@ -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] [--] <token>`
* `talk:room:update [--name NAME] [--description DESCRIPTION] [--public PUBLIC] [--readonly READONLY] [--listable LISTABLE] [--password PASSWORD] [--owner OWNER] [--message-expiration MESSAGE-EXPIRATION] [--preserve PRESERVE] [--] <token>`

| Arguments | Description | Is required | Is array | Default |
|---|---|---|---|---|
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class Capabilities implements IPublicCapability {
'conversation-presets',
'private-reply',
'conversation-tags',
'preserve-conversation',
];

public const CONDITIONAL_FEATURES = [
Expand Down
14 changes: 14 additions & 0 deletions lib/Chat/Parser/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
13 changes: 13 additions & 0 deletions lib/Chat/SystemMessage/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions lib/Command/Room/Create.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
}

Expand All @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
4 changes: 4 additions & 0 deletions lib/Command/Room/TRoomCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions lib/Command/Room/Update.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
}

Expand All @@ -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('<error>Invalid value for option "--public" given.</error>');
return 1;
}

if (!in_array($preserve, [null, '0', '1'], true)) {
$output->writeln('<error>Invalid value for option "--preserve" given.</error>');
return 1;
}

if (!in_array($readOnly, [null, (string)Room::READ_WRITE, (string)Room::READ_ONLY], true)) {
$output->writeln('<error>Invalid value for option "--readonly" given.</error>');
return 1;
Expand Down Expand Up @@ -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('<error>%s</error>', $e->getMessage()));
return 1;
Expand Down
5 changes: 5 additions & 0 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
82 changes: 78 additions & 4 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1026,10 +1026,11 @@ public function setDescription(string $description): DataResponse {
/**
* Delete a room
*
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST, null, array{}>
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST, null, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'preserved'}, array{}>
*
* 200: Room successfully deleted
* 400: Deleting room is not possible
* 403: Conversation is preserved
*/
#[PublicPage]
#[RequireModeratorParticipant]
Expand All @@ -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);
Expand Down Expand Up @@ -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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'|'password', message?: string}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'|'password', message?: string}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'preserved'}, array{}>
*
* 200: Allowed guests successfully
* 400: Allowing guests is not possible
* 403: Conversation is preserved
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
Expand All @@ -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);
}
Expand All @@ -1709,10 +1719,11 @@ public function makePublic(string $password = ''): DataResponse {
/**
* Disallowed guests to join conversation
*
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'preserved'}, array{}>
*
* 200: Room unpublished Disallowing guests successfully
* 400: Disallowing guests is not possible
* 403: Conversation is preserved
*/
#[NoAdminRequired]
#[RequireLoggedInModeratorParticipant]
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}|array{error: 'forced', forced?: 0|1|2}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'breakout-room'|'type'|'value'}|array{error: 'forced', forced?: 0|1|2}, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'preserved'}, array{}>
*
* 200: Made room listable successfully
* 400: Making room listable is not possible
* 403: Conversation is preserved
*/
#[NoAdminRequired]
#[RequireModeratorParticipant]
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'permissions'}, array{}>
*
* 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<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{error: 'permissions'}, array{}>
*
* 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
*
Expand Down
9 changes: 5 additions & 4 deletions lib/Events/ARoomModifiedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
4 changes: 4 additions & 0 deletions lib/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Loading
Loading