Browse Source

feat(retention): Allow defining a default retention for event and phone conversations

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/15018/head
Joas Schilling 6 months ago
parent
commit
0dc1441025
No known key found for this signature in database GPG Key ID: F72FA5B49FFA96B0
  1. 4
      appinfo/info.xml
  2. 2
      appinfo/routes/routesRoomController.php
  3. 3
      docs/capabilities.md
  4. 2
      docs/settings.md
  5. 65
      lib/BackgroundJob/ExpireObjectRooms.php
  6. 5
      lib/Capabilities.php
  7. 32
      lib/Controller/RoomController.php
  8. 2
      lib/Listener/CalDavEventListener.php
  9. 33
      lib/Manager.php
  10. 3
      lib/Notification/Notifier.php
  11. 2
      lib/ResponseDefinitions.php
  12. 7
      lib/Room.php
  13. 2
      lib/Service/AvatarService.php
  14. 11
      lib/Service/RoomService.php
  15. 14
      openapi-administration.json
  16. 14
      openapi-backend-recording.json
  17. 14
      openapi-backend-signaling.json
  18. 14
      openapi-backend-sipbridge.json
  19. 14
      openapi-bots.json
  20. 14
      openapi-federation.json
  21. 139
      openapi-full.json
  22. 139
      openapi.json
  23. 2
      src/__mocks__/capabilities.ts
  24. 5
      src/components/CallView/shared/EmptyCallView.vue
  25. 4
      src/components/ConversationIcon.vue
  26. 3
      src/components/LeftSidebar/CallPhoneDialog/CallPhoneDialog.vue
  27. 6
      src/components/TopBar/CallButton.vue
  28. 5
      src/constants.ts
  29. 4
      src/types/openapi/openapi-administration.ts
  30. 4
      src/types/openapi/openapi-backend-recording.ts
  31. 4
      src/types/openapi/openapi-backend-signaling.ts
  32. 4
      src/types/openapi/openapi-backend-sipbridge.ts
  33. 4
      src/types/openapi/openapi-bots.ts
  34. 4
      src/types/openapi/openapi-federation.ts
  35. 72
      src/types/openapi/openapi-full.ts
  36. 72
      src/types/openapi/openapi.ts
  37. 18
      tests/php/CapabilitiesTest.php
  38. 10
      tests/php/Listener/CalDavEventListenerTest.php

4
appinfo/info.xml

@ -18,7 +18,7 @@
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
]]></description>
<version>21.1.0-dev.4</version>
<version>21.1.0-dev.5</version>
<licence>agpl</licence>
<author>Anna Larch</author>
@ -32,7 +32,6 @@
<author>Marcel Hibbe</author>
<author>Marcel Müller</author>
<author>Sowjanya Kota</author>
<author>Shankar Kalidindi</author>
<namespace>Talk</namespace>
@ -65,6 +64,7 @@
<job>OCA\Talk\BackgroundJob\CheckMatterbridges</job>
<job>OCA\Talk\BackgroundJob\CheckTurnCertificate</job>
<job>OCA\Talk\BackgroundJob\ExpireChatMessages</job>
<job>OCA\Talk\BackgroundJob\ExpireObjectRooms</job>
<job>OCA\Talk\BackgroundJob\ExpireSignalingMessage</job>
<job>OCA\Talk\BackgroundJob\LockInactiveRooms</job>
<job>OCA\Talk\BackgroundJob\MaximumCallDuration</job>

2
appinfo/routes/routesRoomController.php

@ -33,6 +33,8 @@ return [
['name' => 'Room#renameRoom', 'url' => '/api/{apiVersion}/room/{token}', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::deleteRoom() */
['name' => 'Room#deleteRoom', 'url' => '/api/{apiVersion}/room/{token}', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::unbindRoomFromObject() */
['name' => 'Room#unbindRoomFromObject', 'url' => '/api/{apiVersion}/room/{token}/object', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::makePublic() */
['name' => 'Room#makePublic', 'url' => '/api/{apiVersion}/room/{token}/public', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::makePrivate() */

3
docs/capabilities.md

@ -183,4 +183,7 @@
* `conversation-creation-all` (local) - Whether the conversation creation endpoint allows to specify all attributes of a conversation
* `sip-direct-dialin` (local) - Whether the SIP bridge can create conversations when an external participant calls a mapped phone number
* `important-conversations` (local) - Whether important conversations are supported
* `unbind-conversation` (local) - Whether the API exists to make an event and phone conversation persistent
* `config => conversations => retention-event` (local) - Number of days before an inactive event conversation is deleted (`0` = disabled)
* `config => conversations => retention-phone` (local) - Number of days before an inactive incoming or outgoing phone conversation is deleted (`0` = disabled)
* `config => call => predefined-backgrounds-v2` (local) - Whether virtual backgrounds should be read from the theming directory

2
docs/settings.md

@ -99,6 +99,8 @@ Legend:
| `recording_consent` | int | `0` | Yes | 🖌️ | Whether users have to agree on being recorded before they can join the call (see [constants](constants.md#recording-consent-required)) |
| `grid_videos_limit` | int | `19` | No | | Maximum number of videos to show (additional to the own video) |
| `sip_dialin_default` | int | `0` | No | | Default value of SIP dial-in when creating new conversations |
| `retention_event_rooms` | int | `28` | No | | Retention period of event conversations in days (`0` means no-retention) |
| `retention_phone_rooms` | int | `7` | No | | Retention period of phone dial-in and dial-out conversations in days (`0` means no-retention) |
| `grid_videos_limit_enforced` | string<br>`yes` or `no` | `no` | No | | Whether the number of grid videos should be enforced |
| `changelog` | string<br>`yes` or `no` | `yes` | No | | Whether the changelog conversation is updated with new features on major releases |
| `has_reference_id` | string<br>`yes` or `no` | `no` | Yes | | Indicator whether the clients can use the reference value to identify their message, will be automatically set to `yes` when the repair steps are executed |

65
lib/BackgroundJob/ExpireObjectRooms.php

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Talk\BackgroundJob;
use OCA\Talk\Manager;
use OCA\Talk\Room;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
use Psr\Log\LoggerInterface;
class ExpireObjectRooms extends TimedJob {
public function __construct(
ITimeFactory $timeFactory,
protected Manager $manager,
protected RoomService $roomService,
protected LoggerInterface $logger,
protected IAppConfig $appConfig,
) {
parent::__construct($timeFactory);
$this->setInterval(60 * 60 * 24);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
#[\Override]
protected function run($argument): void {
$phoneRetention = $this->appConfig->getAppValueInt('retention_phone_rooms', 7);
if ($phoneRetention !== 0) {
$this->executeRetention(Room::OBJECT_TYPE_PHONE_TEMPORARY, $phoneRetention);
}
$eventRetention = $this->appConfig->getAppValueInt('retention_event_rooms', 28);
if ($eventRetention !== 0) {
$this->executeRetention(Room::OBJECT_TYPE_EVENT, $eventRetention);
}
}
protected function executeRetention(string $objectType, int $retention): void {
$now = $this->time->getTime();
$minimumLastActivity = $now - $retention * 24 * 3600;
$rooms = $this->manager->getExpiringRoomsForObjectType($objectType, $minimumLastActivity);
$numDeletedRooms = 0;
foreach ($rooms as $room) {
$this->roomService->deleteRoom($room);
$numDeletedRooms++;
}
$this->logger->info('Deleted {numDeletedRooms} {objectType} rooms because they did not have activity since {minimumLastActivity} days', [
'objectType' => $objectType,
'numDeletedRooms' => $numDeletedRooms,
'minimumLastActivity' => $retention,
]);
}
}

5
lib/Capabilities.php

@ -116,6 +116,7 @@ class Capabilities implements IPublicCapability {
'edit-draft-poll',
'conversation-creation-all',
'important-conversations',
'unbind-conversation',
'sip-direct-dialin',
];
@ -168,6 +169,8 @@ class Capabilities implements IPublicCapability {
'can-create',
'list-style',
'description-length',
'retention-event',
'retention-phone',
],
'federation' => [
'enabled',
@ -249,6 +252,8 @@ class Capabilities implements IPublicCapability {
'force-passwords' => $this->talkConfig->isPasswordEnforced(),
'list-style' => $this->talkConfig->getConversationsListStyle($user?->getUID()),
'description-length' => Room::DESCRIPTION_MAXIMUM_LENGTH,
'retention-event' => max(0, $this->appConfig->getAppValueInt('retention_event_rooms', 28)),
'retention-phone' => max(0, $this->appConfig->getAppValueInt('retention_phone_rooms', 7)),
],
'federation' => [
'enabled' => false,

32
lib/Controller/RoomController.php

@ -684,7 +684,11 @@ class RoomController extends AEnvironmentAwareOCSController {
$roomType = Room::TYPE_PUBLIC;
}
if ($objectType === Room::OBJECT_TYPE_PHONE) {
if (in_array($objectType, [
Room::OBJECT_TYPE_PHONE_PERSIST,
Room::OBJECT_TYPE_PHONE_TEMPORARY,
Room::OBJECT_TYPE_PHONE_LEGACY,
], true)) {
$objectId = Room::OBJECT_ID_PHONE_OUTGOING;
}
@ -929,6 +933,30 @@ class RoomController extends AEnvironmentAwareOCSController {
return new DataResponse(null);
}
/**
* Unbind a room from its object to prevent automatic retention
*
* Required capability: `unbind-conversation`
*
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'object-type'}, array{}>
*
* 200: Room successfully unbound
* 400: Unbinding room is not possible
*/
#[PublicPage]
#[RequireModeratorParticipant]
public function unbindRoomFromObject(): DataResponse {
if ($this->room->getObjectType() === Room::OBJECT_TYPE_EVENT) {
$this->roomService->resetObject($this->room);
} elseif ($this->room->getObjectType() === Room::OBJECT_TYPE_PHONE_TEMPORARY) {
$this->roomService->setObject($this->room, Room::OBJECT_TYPE_PHONE_PERSIST, $this->room->getObjectId());
} else {
return new DataResponse(['error' => 'object-type'], Http::STATUS_BAD_REQUEST);
}
return new DataResponse($this->formatRoom($this->room, $this->participant));
}
/**
*
* Get a list of participants for a room
@ -2019,7 +2047,7 @@ class RoomController extends AEnvironmentAwareOCSController {
Room::TYPE_GROUP,
$caller,
$user,
Room::OBJECT_TYPE_PHONE,
Room::OBJECT_TYPE_PHONE_TEMPORARY,
Room::OBJECT_ID_PHONE_INCOMING,
sipEnabled: Webinary::SIP_ENABLED_NO_PIN,
);

2
lib/Listener/CalDavEventListener.php

@ -173,6 +173,6 @@ class CalDavEventListener implements IEventListener {
}
$objectId = $start . '#' . $end;
$this->roomService->setObject($room, $objectId, Room::OBJECT_TYPE_EVENT);
$this->roomService->setObject($room, Room::OBJECT_TYPE_EVENT, $objectId);
}
}

33
lib/Manager.php

@ -942,6 +942,39 @@ class Manager {
return $rooms;
}
/**
* @return list<Room>
*/
public function getExpiringRoomsForObjectType(string $objectType, int $minimumLastActivity): array {
$query = $this->db->getQueryBuilder();
$helper = new SelectHelper();
$helper->selectRoomsTable($query);
$query->from('talk_rooms')
->where($query->expr()->eq('object_type', $objectType))
->andWhere($query->expr()->lte('last_activity', $minimumLastActivity));
if ($objectType === Room::OBJECT_TYPE_EVENT) {
// Ignore events that don't have a start and end date,
// as they are most likely from before the Talk 21.1 upgrade
$query->andWhere($query->expr()->like('object_id', '%' . $this->db->escapeLikeParameter('#') . '%'));
}
$result = $query->executeQuery();
$rooms = [];
while ($row = $result->fetch()) {
if ($row['token'] === null) {
// FIXME Temporary solution for the Talk6 release
continue;
}
$rooms[] = $this->createRoomObject($row);
}
$result->closeCursor();
return $rooms;
}
/**
* @param string[] $tokens
* @return array<string, Room>

3
lib/Notification/Notifier.php

@ -1048,7 +1048,8 @@ class Notifier implements INotifier {
} else {
throw new AlreadyProcessedException();
}
} elseif ($room->getObjectType() === Room::OBJECT_TYPE_PHONE && $room->getObjectId() === Room::OBJECT_ID_PHONE_INCOMING) {
} elseif ($room->getObjectId() === Room::OBJECT_ID_PHONE_INCOMING
&& in_array($room->getObjectType(), [Room::OBJECT_TYPE_PHONE_PERSIST, Room::OBJECT_TYPE_PHONE_TEMPORARY, Room::OBJECT_TYPE_PHONE_LEGACY], true)) {
if ($this->notificationManager->isPreparingPushNotification()
|| (!$room->isFederatedConversation() && $this->participantService->hasActiveSessionsInCall($room))
|| ($room->isFederatedConversation() && $room->getActiveSince())

2
lib/ResponseDefinitions.php

@ -380,6 +380,8 @@ namespace OCA\Talk;
* force-passwords: bool,
* list-style: 'two-lines'|'compact',
* description-length: positive-int,
* retention-event: non-negative-int,
* retention-phone: non-negative-int,
* },
* federation: array{
* enabled: bool,

7
lib/Room.php

@ -44,7 +44,12 @@ class Room {
public const OBJECT_TYPE_EXTENDED_CONVERSATION = 'extended_conversation';
public const OBJECT_TYPE_FILE = 'file';
public const OBJECT_TYPE_NOTE_TO_SELF = 'note_to_self';
public const OBJECT_TYPE_PHONE = 'phone';
/**
* @deprecated No longer used for new conversations
*/
public const OBJECT_TYPE_PHONE_LEGACY = 'phone';
public const OBJECT_TYPE_PHONE_PERSIST = 'phone_persist';
public const OBJECT_TYPE_PHONE_TEMPORARY = 'phone_temporary';
public const OBJECT_TYPE_SAMPLE = 'sample';
public const OBJECT_TYPE_VIDEO_VERIFICATION = 'share:password';

2
lib/Service/AvatarService.php

@ -267,7 +267,7 @@ class AvatarService {
if ($room->getObjectType() === Room::OBJECT_TYPE_EMAIL) {
return __DIR__ . '/../../img/icon-conversation-mail-' . $colorTone . '.svg';
}
if ($room->getObjectType() === Room::OBJECT_TYPE_PHONE) {
if (in_array($room->getObjectType(), [Room::OBJECT_TYPE_PHONE_PERSIST, Room::OBJECT_TYPE_PHONE_TEMPORARY, Room::OBJECT_TYPE_PHONE_LEGACY], true)) {
return __DIR__ . '/../../img/icon-conversation-phone-' . $colorTone . '.svg';
}
if ($room->isFederatedConversation()) {

11
lib/Service/RoomService.php

@ -183,7 +183,10 @@ class RoomService {
$objectTypes = [
'',
Room::OBJECT_TYPE_PHONE,
// Kept to keep older clients working
Room::OBJECT_TYPE_PHONE_LEGACY,
Room::OBJECT_TYPE_PHONE_PERSIST,
Room::OBJECT_TYPE_PHONE_TEMPORARY,
Room::OBJECT_TYPE_EVENT,
Room::OBJECT_TYPE_EXTENDED_CONVERSATION,
];
@ -1445,10 +1448,14 @@ class RoomService {
$room->setObjectType('');
}
public function setObject(Room $room, string $objectId = '', string $objectType = ''): void {
/**
* @psalm-param Room::OBJECT_TYPE_* $objectType
*/
public function setObject(Room $room, string $objectType, string $objectId): void {
if (($objectId !== '' && $objectType === '') || ($objectId === '' && $objectType !== '')) {
throw new InvalidRoomException('Object ID and Object Type must both be empty or both have values');
}
$update = $this->db->getQueryBuilder();
$update->update('talk_rooms')
->set('object_id', $update->createNamedParameter($objectId, IQueryBuilder::PARAM_STR))

14
openapi-administration.json

@ -257,7 +257,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -277,6 +279,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

14
openapi-backend-recording.json

@ -190,7 +190,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -210,6 +212,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

14
openapi-backend-signaling.json

@ -190,7 +190,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -210,6 +212,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

14
openapi-backend-sipbridge.json

@ -233,7 +233,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -253,6 +255,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

14
openapi-bots.json

@ -190,7 +190,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -210,6 +212,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

14
openapi-federation.json

@ -233,7 +233,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -253,6 +255,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},

139
openapi-full.json

@ -391,7 +391,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -411,6 +413,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
@ -13099,6 +13111,131 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/object": {
"delete": {
"operationId": "room-unbind-room-from-object",
"summary": "Unbind a room from its object to prevent automatic retention",
"description": "Required capability: `unbind-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": "Room successfully unbound",
"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"
}
}
}
}
}
}
}
},
"400": {
"description": "Unbinding room is not possible",
"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": [
"object-type"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/public": {
"post": {
"operationId": "room-make-public",

139
openapi.json

@ -350,7 +350,9 @@
"can-create",
"force-passwords",
"list-style",
"description-length"
"description-length",
"retention-event",
"retention-phone"
],
"properties": {
"can-create": {
@ -370,6 +372,16 @@
"type": "integer",
"format": "int64",
"minimum": 1
},
"retention-event": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"retention-phone": {
"type": "integer",
"format": "int64",
"minimum": 0
}
}
},
@ -13004,6 +13016,131 @@
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/object": {
"delete": {
"operationId": "room-unbind-room-from-object",
"summary": "Unbind a room from its object to prevent automatic retention",
"description": "Required capability: `unbind-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": "Room successfully unbound",
"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"
}
}
}
}
}
}
}
},
"400": {
"description": "Unbinding room is not possible",
"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": [
"object-type"
]
}
}
}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/public": {
"post": {
"operationId": "room-make-public",

2
src/__mocks__/capabilities.ts

@ -149,6 +149,8 @@ export const mockedCapabilities: Capabilities = {
'force-passwords': false,
'list-style': 'two-lines',
'description-length': 2000,
'retention-event': 28,
'retention-phone': 7,
},
federation: {
enabled: false,

5
src/components/CallView/shared/EmptyCallView.vue

@ -102,7 +102,10 @@ export default {
},
isPhoneConversation() {
return this.conversation && this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE
return this.conversation
&& (this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_LEGACY
|| this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_PERSISTENT
|| this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_TEMPORARY)
},
conversationDisplayName() {

4
src/components/ConversationIcon.vue

@ -186,7 +186,9 @@ export default {
return 'icon-password'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.EMAIL) {
return 'icon-mail'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.PHONE) {
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.PHONE_LEGACY
|| this.item.objectType === CONVERSATION.OBJECT_TYPE.PHONE_PERSISTENT
|| this.item.objectType === CONVERSATION.OBJECT_TYPE.PHONE_TEMPORARY) {
return 'icon-phone'
} else if (this.item.objectType === CONVERSATION.OBJECT_TYPE.CIRCLES) {
return 'icon-team'

3
src/components/LeftSidebar/CallPhoneDialog/CallPhoneDialog.vue

@ -56,6 +56,7 @@ import DialpadPanel from '../../UIShared/DialpadPanel.vue'
import { CONVERSATION, PARTICIPANT } from '../../../constants.ts'
import { callSIPDialOut } from '../../../services/callsService.js'
import { hasTalkFeature } from '../../../services/CapabilitiesManager.ts'
import { createLegacyConversation } from '../../../services/conversationsService.ts'
import { addParticipant } from '../../../services/participantsService.js'
@ -123,7 +124,7 @@ export default {
const response = await createLegacyConversation({
roomType: CONVERSATION.TYPE.GROUP,
roomName: this.participantPhoneItem.phoneNumber,
objectType: CONVERSATION.OBJECT_TYPE.PHONE,
objectType: hasTalkFeature('local', 'sip-direct-dialin') ? CONVERSATION.OBJECT_TYPE.PHONE_TEMPORARY : CONVERSATION.OBJECT_TYPE.PHONE_LEGACY,
})
conversation = response.data.ocs.data
await this.$store.dispatch('addConversation', conversation)

6
src/components/TopBar/CallButton.vue

@ -340,8 +340,10 @@ export default {
},
isPhoneRoom() {
return this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE
&& this.conversation.objectId === CONVERSATION.OBJECT_ID.PHONE_OUTGOING
return this.conversation.objectId === CONVERSATION.OBJECT_ID.PHONE_OUTGOING
&& (this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_LEGACY
|| this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_PERSISTENT
|| this.conversation.objectType === CONVERSATION.OBJECT_TYPE.PHONE_TEMPORARY)
},
isInLobby() {

5
src/constants.ts

@ -99,7 +99,10 @@ export const CONVERSATION = {
OBJECT_TYPE: {
EMAIL: 'emails',
FILE: 'file',
PHONE: 'phone',
/** @deprecated */
PHONE_LEGACY: 'phone',
PHONE_PERSISTENT: 'phone_persist',
PHONE_TEMPORARY: 'phone_temporary',
CIRCLES: 'circles',
VIDEO_VERIFICATION: 'share:password',
BREAKOUT_ROOM: 'room',

4
src/types/openapi/openapi-administration.ts

@ -256,6 +256,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

4
src/types/openapi/openapi-backend-recording.ts

@ -90,6 +90,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

4
src/types/openapi/openapi-backend-signaling.ts

@ -76,6 +76,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

4
src/types/openapi/openapi-backend-sipbridge.ts

@ -191,6 +191,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

4
src/types/openapi/openapi-bots.ts

@ -94,6 +94,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

4
src/types/openapi/openapi-federation.ts

@ -202,6 +202,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;

72
src/types/openapi/openapi-full.ts

@ -891,6 +891,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/object": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Unbind a room from its object to prevent automatic retention
* @description Required capability: `unbind-conversation`
*/
delete: operations["room-unbind-room-from-object"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/public": {
parameters: {
query?: never;
@ -2108,6 +2128,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;
@ -7005,6 +7029,54 @@ export interface operations {
};
};
};
"room-unbind-room-from-object": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Room successfully unbound */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
/** @description Unbinding room is not possible */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "object-type";
};
};
};
};
};
};
};
"room-make-public": {
parameters: {
query?: never;

72
src/types/openapi/openapi.ts

@ -891,6 +891,26 @@ export type paths = {
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/object": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/**
* Unbind a room from its object to prevent automatic retention
* @description Required capability: `unbind-conversation`
*/
delete: operations["room-unbind-room-from-object"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/public": {
parameters: {
query?: never;
@ -1586,6 +1606,10 @@ export type components = {
"list-style": "two-lines" | "compact";
/** Format: int64 */
"description-length": number;
/** Format: int64 */
"retention-event": number;
/** Format: int64 */
"retention-phone": number;
};
federation: {
enabled: boolean;
@ -6467,6 +6491,54 @@ export interface operations {
};
};
};
"room-unbind-room-from-object": {
parameters: {
query?: never;
header: {
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
path: {
apiVersion: "v4";
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Room successfully unbound */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: components["schemas"]["Room"];
};
};
};
};
/** @description Unbinding room is not possible */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
ocs: {
meta: components["schemas"]["OCSMeta"];
data: {
/** @enum {string} */
error: "object-type";
};
};
};
};
};
};
};
"room-make-public": {
parameters: {
query?: never;

18
tests/php/CapabilitiesTest.php

@ -109,6 +109,13 @@ class CapabilitiesTest extends TestCase {
['core', 'backgroundjobs_mode', 'ajax', 'cron'],
]);
$this->appConfig->method('getAppValueInt')
->willReturnMap([
['max_call_duration', 0, 0],
['retention_event_rooms', 28, 28],
['retention_phone_rooms', 7, 7],
]);
$this->assertInstanceOf(IPublicCapability::class, $capabilities);
$this->assertSame([
'spreed' => [
@ -171,6 +178,8 @@ class CapabilitiesTest extends TestCase {
'force-passwords' => false,
'list-style' => 'two-lines',
'description-length' => 2000,
'retention-event' => 28,
'retention-phone' => 7,
],
'federation' => [
'enabled' => false,
@ -263,6 +272,13 @@ class CapabilitiesTest extends TestCase {
['backgrounds_upload_users', true, true],
]);
$this->appConfig->method('getAppValueInt')
->willReturnMap([
['max_call_duration', 0, 0],
['retention_event_rooms', 28, 28],
['retention_phone_rooms', 7, 7],
]);
$this->assertInstanceOf(IPublicCapability::class, $capabilities);
$data = $capabilities->getCapabilities();
$this->assertSame([
@ -328,6 +344,8 @@ class CapabilitiesTest extends TestCase {
'force-passwords' => false,
'list-style' => 'two-lines',
'description-length' => 2000,
'retention-event' => 28,
'retention-phone' => 7,
],
'federation' => [
'enabled' => false,

10
tests/php/Listener/CalDavEventListenerTest.php

@ -342,7 +342,7 @@ EOD;
$calData = str_replace('{{{LOCATION}}}', $roomUrl, $this->calData);
$event = new CalendarObjectUpdatedEvent(1, [], [], ['calendardata' => $calData]);
$room = $this->createMock(Room::class);
$room->method('getObjectType')->willReturn(Room::OBJECT_TYPE_PHONE);
$room->method('getObjectType')->willReturn(Room::OBJECT_TYPE_PHONE_LEGACY);
$participant = $this->createMock(Participant::class);
$participant->method('hasModeratorPermissions')->willReturn(true);
@ -505,7 +505,7 @@ EOF;
->method('resetObject');
$this->roomService->expects(self::once())
->method('setObject')
->with($room, '1741942800#1741946400', Room::OBJECT_TYPE_EVENT);
->with($room, Room::OBJECT_TYPE_EVENT, '1741942800#1741946400');
$this->timezoneService->expects(self::never())
->method('getUserTimezone');
$this->logger->expects(self::never())
@ -557,7 +557,7 @@ EOF;
->method('resetObject');
$this->roomService->expects(self::once())
->method('setObject')
->with($room, '1741820400#1741906800', Room::OBJECT_TYPE_EVENT);
->with($room, Room::OBJECT_TYPE_EVENT, '1741820400#1741906800');
$this->timezoneService->expects(self::once())
->method('getUserTimezone')
->willReturn('Europe/Vienna');
@ -610,7 +610,7 @@ EOF;
->method('resetObject');
$this->roomService->expects(self::once())
->method('setObject')
->with($room, '1741820400#1741906800', Room::OBJECT_TYPE_EVENT);
->with($room, Room::OBJECT_TYPE_EVENT, '1741820400#1741906800');
$this->timezoneService->expects(self::once())
->method('getUserTimezone')
->willReturn(null);
@ -664,7 +664,7 @@ EOF;
->method('resetObject');
$this->roomService->expects(self::once())
->method('setObject')
->with($room, '1741824000#1741910400', Room::OBJECT_TYPE_EVENT);
->with($room, Room::OBJECT_TYPE_EVENT, '1741824000#1741910400');
$this->timezoneService->expects(self::once())
->method('getUserTimezone')
->willReturn(null);

Loading…
Cancel
Save