diff --git a/docs/settings.md b/docs/settings.md
index e95738b25a3..b9e402483fa 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -112,6 +112,7 @@ Legend:
| `federation_only_trusted_servers` | string
`1` or `0` | `0` | Yes | | 🏗️ *Work in progress:* Whether federation should be limited to the list of "Trusted servers" |
| `conversations_files` | string
`1` or `0` | `1` | No | 🖌️ | Whether the files app integration is enabled allowing to start conversations in the right sidebar |
| `conversations_files_public_shares` | string
`1` or `0` | `1` | No | 🖌️ | Whether the public share integration is enabled allowing to start conversations in the right sidebar on the public share page (Requires `conversations_files` also to be enabled) |
+| `delete_one_to_one_conversations` | string
`1` or `0` | `0` | No | ️ | Whether one-to-one conversations can be left by either participant or should be deleted when one participant leaves |
| `enable_matterbridge` | string
`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured |
| `force_passwords` | string
`1` or `0` | `0` | No | ️ | Whether public chats are forced to use a password |
| `inactivity_lock_after_days` | int | `0` | No | | A duration (in days) after which rooms are locked. Calculated from the last activity in the room. |
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index 6645ebd09d1..9ae1227d1b3 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -1185,10 +1185,15 @@ protected function validateMessageExists(int $messageId, bool $sync = false): vo
#[RequireReadWriteConversation]
public function clearHistory(): DataResponse {
$attendee = $this->participant->getAttendee();
- if (!$this->participant->hasModeratorPermissions(false)
- || $this->room->getType() === Room::TYPE_ONE_TO_ONE
- || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
- // Actor is not a moderator or not the owner of the message
+ if (!$this->participant->hasModeratorPermissions(false)) {
+ // Actor is not a moderator
+ 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)) {
+ // Not allowed to purge one-to-one conversations
return new DataResponse(null, Http::STATUS_FORBIDDEN);
}
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index 70b353c7b06..382fc7f83fe 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -70,6 +70,7 @@
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudIdManager;
@@ -116,6 +117,7 @@ public function __construct(
protected ChecksumVerificationService $checksumVerificationService,
protected RoomFormatter $roomFormatter,
protected IConfig $config,
+ protected IAppConfig $appConfig,
protected Config $talkConfig,
protected ICloudIdManager $cloudIdManager,
protected IPhoneNumberUtil $phoneNumberUtil,
@@ -857,7 +859,8 @@ public function setDescription(string $description): DataResponse {
#[PublicPage]
#[RequireModeratorParticipant]
public function deleteRoom(): DataResponse {
- if ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
+ 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);
}
@@ -1375,7 +1378,7 @@ protected function removeSelfFromRoomLogic(Room $room, Participant $participant)
}
if ($room->getType() !== Room::TYPE_CHANGELOG &&
- $room->getObjectType() !== 'file' &&
+ $room->getObjectType() !== Room::OBJECT_TYPE_FILE &&
$this->participantService->getNumberOfUsers($room) === 1 &&
\in_array($participant->getAttendee()->getParticipantType(), [
Participant::USER,
@@ -1386,6 +1389,12 @@ protected function removeSelfFromRoomLogic(Room $room, Participant $participant)
return new DataResponse(null);
}
+ 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)) {
+ $this->roomService->deleteRoom($room);
+ return new DataResponse(null);
+ }
+
$currentUser = $this->userManager->get($this->userId);
if (!$currentUser instanceof IUser) {
return new DataResponse(['error' => 'participant'], Http::STATUS_NOT_FOUND);
diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php
index 22fcff9852d..bf1ea0ad7a5 100644
--- a/lib/Service/RoomFormatter.php
+++ b/lib/Service/RoomFormatter.php
@@ -20,6 +20,7 @@
use OCA\Talk\Webinary;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\IConfig;
@@ -36,6 +37,7 @@
class RoomFormatter {
public function __construct(
protected Config $talkConfig,
+ protected IAppConfig $appConfig,
protected AvatarService $avatarService,
protected ParticipantService $participantService,
protected ChatManager $chatManager,
@@ -320,6 +322,13 @@ public function formatRoomV4(
&& $room->getType() !== Room::TYPE_ONE_TO_ONE_FORMER
&& $currentParticipant->hasModeratorPermissions(false);
$roomData['canLeaveConversation'] = $room->getType() !== Room::TYPE_NOTE_TO_SELF;
+
+ if ($this->appConfig->getAppValueBool('delete_one_to_one_conversations')
+ && in_array($room->getType(), [Room::TYPE_ONE_TO_ONE, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
+ $roomData['canDeleteConversation'] = true;
+ $roomData['canLeaveConversation'] = false;
+ }
+
$roomData['canEnableSIP'] =
$this->talkConfig->isSIPConfigured()
&& !preg_match(Room::SIP_INCOMPATIBLE_REGEX, $room->getToken())
diff --git a/tests/integration/features/chat-1/delete.feature b/tests/integration/features/chat-1/delete.feature
index 4c3cd8d36e8..a50baa9307a 100644
--- a/tests/integration/features/chat-1/delete.feature
+++ b/tests/integration/features/chat-1/delete.feature
@@ -235,3 +235,13 @@ Feature: chat/delete
Then user "participant2" sees the following system messages in room "room1" with 200 (v1)
| room | actorType | actorId | actorDisplayName | systemMessage |
| room1 | users | participant1 | participant1-displayname | history_cleared |
+
+ Scenario: Can delete chat history in one-to-one conversations when config is set
+ Given user "participant1" creates room "room" with 201 (v4)
+ | roomType | 1 |
+ | invite | participant2 |
+ And user "participant1" sends message "Message" to room "room" with 201
+ Then user "participant1" deletes chat history for room "room" with 403
+ When the following "spreed" app config is set
+ | delete_one_to_one_conversations | 1 |
+ Then user "participant1" deletes chat history for room "room" with 200
diff --git a/tests/integration/features/conversation-3/one-to-one.feature b/tests/integration/features/conversation-3/one-to-one.feature
index b053ad93d8d..996b96d9687 100644
--- a/tests/integration/features/conversation-3/one-to-one.feature
+++ b/tests/integration/features/conversation-3/one-to-one.feature
@@ -198,7 +198,7 @@ Feature: conversation-2/one-to-one
| users | participant1 | 1 |
| users | participant2 | 1 |
- Scenario: Check share restrictions on one to one conversatio
+ Scenario: Check share restrictions on one to one conversation
Given the following "core" app config is set
| shareapi_restrict_user_enumeration_full_match | no |
| shareapi_allow_share_dialog_user_enumeration | yes |
@@ -207,3 +207,33 @@ Feature: conversation-2/one-to-one
And user "participant1" creates room "room15" with 403 (v4)
| roomType | 1 |
| invite | participant2 |
+
+ Scenario: Remove self from one-to-one conversations when deletable config is set deletes it
+ Given user "participant1" creates room "room" with 201 (v4)
+ | roomType | 1 |
+ | invite | participant2 |
+ Then user "participant1" removes themselves from room "room" with 200 (v4)
+ And user "participant1" is participant of the following rooms (v4)
+ And user "participant2" is participant of the following rooms (v4)
+ | id | type | participantType |
+ | room | 1 | 1 |
+ When user "participant1" creates room "room" with 200 (v4)
+ | roomType | 1 |
+ | invite | participant2 |
+ And the following "spreed" app config is set
+ | delete_one_to_one_conversations | 1 |
+ Then user "participant1" removes themselves from room "room" with 200 (v4)
+ And user "participant1" is participant of the following rooms (v4)
+ And user "participant2" is participant of the following rooms (v4)
+
+ Scenario: Deleting one-to-one conversations is possible when deletable config is set
+ Given user "participant1" creates room "room" with 201 (v4)
+ | roomType | 1 |
+ | invite | participant2 |
+ And user "participant1" sends message "Message" to room "room" with 201
+ Then user "participant1" deletes room "room" with 400 (v4)
+ When the following "spreed" app config is set
+ | delete_one_to_one_conversations | 1 |
+ Then user "participant1" deletes room "room" with 200 (v4)
+ And user "participant1" is participant of the following rooms (v4)
+ And user "participant2" is participant of the following rooms (v4)