mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
feat(conversation): Add new API endpoint that allows to provide all settings
Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
parent
21f3a7475f
commit
fd677f258a
18 changed files with 1115 additions and 256 deletions
|
|
@ -178,3 +178,6 @@
|
|||
* `call-end-to-end-encryption` - Signaling support of the server for the end-to-end encryption of calls
|
||||
* `config => call => end-to-end-encryption` - Whether calls should be end-to-end encrypted (currently off by default, until all Talk mobile clients support it)
|
||||
+ `edit-draft-poll` - Whether moderators can edit draft polls
|
||||
|
||||
## 21.1
|
||||
* `conversation-creation-all` - Whether the conversation creation endpoint allows to specify all attributes of a conversation
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ class Capabilities implements IPublicCapability {
|
|||
'call-notification-state-api',
|
||||
'schedule-meeting',
|
||||
'edit-draft-poll',
|
||||
'conversation-creation-all',
|
||||
];
|
||||
|
||||
public const CONDITIONAL_FEATURES = [
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Talk\Controller;
|
||||
|
||||
use OCA\Circles\Model\Circle;
|
||||
use OCA\Talk\Capabilities;
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Events\AAttendeeRemovedEvent;
|
||||
|
|
@ -20,6 +21,7 @@ use OCA\Talk\Exceptions\InvalidPasswordException;
|
|||
use OCA\Talk\Exceptions\ParticipantNotFoundException;
|
||||
use OCA\Talk\Exceptions\ParticipantProperty\PermissionsException;
|
||||
use OCA\Talk\Exceptions\RoomNotFoundException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\CreationException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\DefaultPermissionsException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\DescriptionException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\ListableException;
|
||||
|
|
@ -55,6 +57,7 @@ use OCA\Talk\Room;
|
|||
use OCA\Talk\Service\BanService;
|
||||
use OCA\Talk\Service\BreakoutRoomService;
|
||||
use OCA\Talk\Service\ChecksumVerificationService;
|
||||
use OCA\Talk\Service\InvitationService;
|
||||
use OCA\Talk\Service\NoteToSelfService;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RecordingService;
|
||||
|
|
@ -95,7 +98,9 @@ use Psr\Log\LoggerInterface;
|
|||
/**
|
||||
* @psalm-import-type TalkCapabilities from ResponseDefinitions
|
||||
* @psalm-import-type TalkParticipant from ResponseDefinitions
|
||||
* @psalm-import-type TalkInvitationList from ResponseDefinitions
|
||||
* @psalm-import-type TalkRoom from ResponseDefinitions
|
||||
* @psalm-import-type TalkRoomWithInvalidInvitations from ResponseDefinitions
|
||||
*/
|
||||
class RoomController extends AEnvironmentAwareOCSController {
|
||||
protected array $commonReadMessages = [];
|
||||
|
|
@ -112,6 +117,7 @@ class RoomController extends AEnvironmentAwareOCSController {
|
|||
protected RoomService $roomService,
|
||||
protected BreakoutRoomService $breakoutRoomService,
|
||||
protected NoteToSelfService $noteToSelfService,
|
||||
protected InvitationService $invitationService,
|
||||
protected ParticipantService $participantService,
|
||||
protected SessionService $sessionService,
|
||||
protected GuestManager $guestManager,
|
||||
|
|
@ -507,57 +513,180 @@ class RoomController extends AEnvironmentAwareOCSController {
|
|||
/**
|
||||
* Create a room with a user, a group or a circle
|
||||
*
|
||||
* With the `conversation-creation-all` capability a lot of new options where
|
||||
* introduced.
|
||||
* Before that only `$roomType`, `$roomName`, `$objectType` and `$objectId`
|
||||
* were supported all the time, and `$password` with the
|
||||
* `conversation-creation-password` capability
|
||||
* In case the `$roomType` is {@see Room::TYPE_ONE_TO_ONE} only the `$invite`
|
||||
* or `$participants` parameter is supported.
|
||||
*
|
||||
* @param int $roomType Type of the room
|
||||
* @psalm-param Room::TYPE_* $roomType
|
||||
* @param string $invite User, group, … ID to invite
|
||||
* @param string $roomName Name of the room
|
||||
* @param 'groups'|'circles'|'' $source Source of the invite ID ('circles' to create a room with a circle, etc.)
|
||||
* @param string $objectType Type of the object
|
||||
* @param string $objectId ID of the object
|
||||
* @param string $password The room password (only available with `conversation-creation-password` capability)
|
||||
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'invite'|'mode'|'object'|'password'|'permissions'|'room'|'type', message?: string}, array{}>
|
||||
* @param string $invite User, group, … ID to invite **Deprecated** Use the `$participants` array instead
|
||||
* @param string $roomName Name of the room, unless the legacy mode providing `$invite` and `$source` is used, the name must no longer be empty with the `conversation-creation-all` capability (Ignored if `$roomType` is {@see Room::TYPE_ONE_TO_ONE})
|
||||
* @param 'groups'|'circles'|'' $source Source of the invite ID ('circles' to create a room with a circle, etc.) **Deprecated** Use the `$participants` array instead
|
||||
* @param string $objectType Type of the object (Ignored if `$roomType` is {@see Room::TYPE_ONE_TO_ONE})
|
||||
* @param string $objectId ID of the object (Ignored if `$roomType` is {@see Room::TYPE_ONE_TO_ONE})
|
||||
* @param string $password The room password (only available with `conversation-creation-password` capability) (Ignored if `$roomType` is not {@see Room::TYPE_PUBLIC})
|
||||
* @param 0|1 $readOnly Read only state of the conversation (Default writable) (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param Room::READ_* $readOnly
|
||||
* @param 0|1|2 $listable Scope where the conversation is listable (Default not listable for anyone) (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param Room::LISTABLE_* $listable
|
||||
* @param int $messageExpiration Seconds after which messages will disappear, 0 disables expiration (Default 0) (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param non-negative-int $messageExpiration
|
||||
* @param 0|1 $lobbyState Lobby state of the conversation (Default lobby is disabled) (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param Webinary::LOBBY_* $lobbyState
|
||||
* @param int|null $lobbyTimer Timer when the lobby will be removed (Default null, will not be disabled automatically) (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param non-negative-int|null $lobbyTimer
|
||||
* @param 0|1|2 $sipEnabled Whether SIP dial-in shall be enabled (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param Webinary::SIP_* $sipEnabled
|
||||
* @param int<0, 255> $permissions Default permissions for participants (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param int-mask-of<Attendee::PERMISSIONS_*> $permissions
|
||||
* @param 0|1 $recordingConsent Whether participants need to agree to a recording before joining a call (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param RecordingService::CONSENT_REQUIRED_NO|RecordingService::CONSENT_REQUIRED_YES $recordingConsent
|
||||
* @param 0|1 $mentionPermissions Who can mention at-all in the chat (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param Room::MENTION_PERMISSIONS_* $mentionPermissions
|
||||
* @param string $description Description for the conversation (limited to 2.000 characters) (only available with `conversation-creation-all` capability)
|
||||
* @param ?non-empty-string $emoji Emoji for the avatar of the conversation (only available with `conversation-creation-all` capability)
|
||||
* @param ?non-empty-string $avatarColor Background color of the avatar (Only considered when an emoji was provided) (only available with `conversation-creation-all` capability)
|
||||
* @param array<string, list<string>> $participants List of participants to add grouped by type (only available with `conversation-creation-all` capability)
|
||||
* @psalm-param TalkInvitationList $participants
|
||||
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_ACCEPTED, TalkRoomWithInvalidInvitations, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array{error: 'avatar'|'description'|'invite'|'listable'|'lobby'|'lobby-timer'|'mention-permissions'|'message-expiration'|'name'|'object'|'object-id'|'object-type'|'password'|'permissions'|'read-only'|'recording-consent'|'sip-enabled'|'type', message?: string}, array{}>
|
||||
*
|
||||
* 200: Room already existed
|
||||
* 201: Room created successfully
|
||||
* 202: Room created successfully but not all participants could be added
|
||||
* 400: Room type invalid or missing or invalid password
|
||||
* 403: Missing permissions to create room
|
||||
* 404: User, group or other target to invite was not found
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function createRoom(
|
||||
int $roomType,
|
||||
string $invite = '',
|
||||
int $roomType = Room::TYPE_GROUP,
|
||||
string $invite = '', /* @deprecated */
|
||||
string $roomName = '',
|
||||
string $source = '',
|
||||
string $source = '', /* @deprecated */
|
||||
string $objectType = '',
|
||||
string $objectId = '',
|
||||
string $password = '',
|
||||
int $readOnly = Room::READ_WRITE,
|
||||
int $listable = Room::LISTABLE_NONE,
|
||||
int $messageExpiration = 0,
|
||||
int $lobbyState = Webinary::LOBBY_NONE,
|
||||
?int $lobbyTimer = null,
|
||||
int $sipEnabled = Webinary::SIP_DISABLED,
|
||||
int $permissions = Attendee::PERMISSIONS_DEFAULT,
|
||||
int $recordingConsent = RecordingService::CONSENT_REQUIRED_NO,
|
||||
int $mentionPermissions = Room::MENTION_PERMISSIONS_EVERYONE,
|
||||
string $description = '',
|
||||
?string $emoji = null,
|
||||
?string $avatarColor = null,
|
||||
array $participants = [],
|
||||
): DataResponse {
|
||||
if ($roomType !== Room::TYPE_ONE_TO_ONE) {
|
||||
/** @var IUser $user */
|
||||
$user = $this->userManager->get($this->userId);
|
||||
if ($roomType === Room::TYPE_ONE_TO_ONE) {
|
||||
if ($invite === ''
|
||||
&& isset($participants['users'][0])
|
||||
&& is_string($participants['users'][0])) {
|
||||
$invite = $participants['users'][0];
|
||||
}
|
||||
|
||||
if ($this->talkConfig->isNotAllowedToCreateConversations($user)) {
|
||||
return new DataResponse(['error' => 'permissions'], Http::STATUS_FORBIDDEN);
|
||||
return $this->createOneToOneRoom($invite);
|
||||
}
|
||||
|
||||
/** @var IUser $user */
|
||||
$user = $this->userManager->get($this->userId);
|
||||
|
||||
if ($this->talkConfig->isNotAllowedToCreateConversations($user)) {
|
||||
return new DataResponse(['error' => 'permissions'], Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
if ($invite !== '') {
|
||||
// Legacy fallback for creating a conversation directly with a group or team
|
||||
if ($source === 'circles') {
|
||||
$sourceV2 = 'teams';
|
||||
} else {
|
||||
$sourceV2 = 'groups';
|
||||
}
|
||||
if (!isset($participants[$sourceV2])) {
|
||||
$participants[$sourceV2] = [];
|
||||
}
|
||||
$participants[$sourceV2][] = $invite;
|
||||
$participants[$sourceV2] = array_values(array_unique($participants[$sourceV2]));
|
||||
}
|
||||
|
||||
if ($roomName === '') {
|
||||
// Legacy fallback for creating a conversation without a name
|
||||
$roomName = $this->roomService->prepareConversationName($invite ?: '---');
|
||||
if ($source === 'circles') {
|
||||
try {
|
||||
$circle = $this->participantService->getCircle($invite, $this->userId);
|
||||
$roomName = $this->roomService->prepareConversationName($circle->getName());
|
||||
} catch (\Exception) {
|
||||
}
|
||||
} else {
|
||||
$targetGroup = $this->groupManager->get($invite);
|
||||
if ($targetGroup instanceof IGroup) {
|
||||
$roomName = $this->roomService->prepareConversationName($targetGroup->getDisplayName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch ($roomType) {
|
||||
case Room::TYPE_ONE_TO_ONE:
|
||||
return $this->createOneToOneRoom($invite);
|
||||
case Room::TYPE_GROUP:
|
||||
if ($invite === '') {
|
||||
return $this->createEmptyRoom($roomName, false, $objectType, $objectId);
|
||||
}
|
||||
if ($source === 'circles') {
|
||||
return $this->createCircleRoom($invite);
|
||||
}
|
||||
return $this->createGroupRoom($invite);
|
||||
case Room::TYPE_PUBLIC:
|
||||
return $this->createEmptyRoom($roomName, true, $objectType, $objectId, $password);
|
||||
if ($roomType !== Room::TYPE_PUBLIC) {
|
||||
// Force empty password for non-public conversations
|
||||
$password = '';
|
||||
}
|
||||
|
||||
return new DataResponse(['error' => 'type'], Http::STATUS_BAD_REQUEST);
|
||||
$invitationList = $this->invitationService->validateInvitations($participants, $user);
|
||||
if ($invitationList->hasInvalidInvitations() && !$invitationList->hasValidInvitations()) {
|
||||
// FIXME add the list of failed invitations?
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!empty($invitationList->getEmails())) {
|
||||
$roomType = Room::TYPE_PUBLIC;
|
||||
}
|
||||
|
||||
try {
|
||||
$room = $this->roomService->createConversation(
|
||||
$roomType,
|
||||
$roomName,
|
||||
$user,
|
||||
$objectType,
|
||||
$objectId,
|
||||
$password,
|
||||
$readOnly,
|
||||
$listable,
|
||||
$messageExpiration,
|
||||
$lobbyState,
|
||||
$lobbyTimer,
|
||||
$sipEnabled,
|
||||
$permissions,
|
||||
$recordingConsent,
|
||||
$mentionPermissions,
|
||||
$description,
|
||||
$emoji,
|
||||
$avatarColor,
|
||||
allowInternalTypes: false,
|
||||
);
|
||||
} catch (CreationException $e) {
|
||||
return new DataResponse(['error' => $e->getReason()], Http::STATUS_BAD_REQUEST);
|
||||
} catch (PasswordException $e) {
|
||||
return new DataResponse(['error' => 'password', 'message' => $e->getHint()], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if ($invitationList->hasValidInvitations()) {
|
||||
$this->participantService->addInvitationList($room, $invitationList, $user);
|
||||
}
|
||||
|
||||
if (!$invitationList->hasInvalidInvitations()) {
|
||||
return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $this->userId, false)), Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
$data = $this->formatRoom($room, $this->participantService->getParticipant($room, $this->userId, false));
|
||||
$data['invalidParticipants'] = $invitationList->getInvalidList();
|
||||
|
||||
return new DataResponse($data, Http::STATUS_ACCEPTED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -609,139 +738,6 @@ class RoomController extends AEnvironmentAwareOCSController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a group video call from the selected group
|
||||
*
|
||||
* @param string $targetGroupName
|
||||
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: 'invite'}, array{}>
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
protected function createGroupRoom(string $targetGroupName): DataResponse {
|
||||
$currentUser = $this->userManager->get($this->userId);
|
||||
if (!$currentUser instanceof IUser) {
|
||||
// Should never happen, basically an internal server error so we reuse another error
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
$targetGroup = $this->groupManager->get($targetGroupName);
|
||||
if (!$targetGroup instanceof IGroup) {
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Create the room
|
||||
$name = $this->roomService->prepareConversationName($targetGroup->getDisplayName());
|
||||
$room = $this->roomService->createConversation(Room::TYPE_GROUP, $name, $currentUser);
|
||||
$this->participantService->addGroup($room, $targetGroup);
|
||||
|
||||
return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)), Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a group video call from the selected circle
|
||||
*
|
||||
* @param string $targetCircleId
|
||||
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: 'invite'}, array{}>
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
protected function createCircleRoom(string $targetCircleId): DataResponse {
|
||||
if (!$this->appManager->isEnabledForUser('circles')) {
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$currentUser = $this->userManager->get($this->userId);
|
||||
if (!$currentUser instanceof IUser) {
|
||||
// Should never happen, basically an internal server error so we reuse another error
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$circle = $this->participantService->getCircle($targetCircleId, $this->userId);
|
||||
} catch (\Exception $e) {
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Create the room
|
||||
$name = $this->roomService->prepareConversationName($circle->getName());
|
||||
$room = $this->roomService->createConversation(Room::TYPE_GROUP, $name, $currentUser);
|
||||
$this->participantService->addCircle($room, $circle);
|
||||
|
||||
return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)), Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataResponse<Http::STATUS_CREATED, TalkRoom, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{error: 'invite'|'mode'|'object'|'password'|'permissions'|'room', message?: string}, array{}>
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = '', string $password = ''): DataResponse {
|
||||
$currentUser = $this->userManager->get($this->userId);
|
||||
if (!$currentUser instanceof IUser) {
|
||||
// Should never happen, basically an internal server error so we reuse another error
|
||||
return new DataResponse(['error' => 'invite'], Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
$roomType = $public ? Room::TYPE_PUBLIC : Room::TYPE_GROUP;
|
||||
/** @var Room|null $parentRoom */
|
||||
$parentRoom = null;
|
||||
|
||||
if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
|
||||
try {
|
||||
$parentRoom = $this->manager->getRoomForUserByToken($objectId, $this->userId);
|
||||
$parentRoomParticipant = $this->participantService->getParticipant($parentRoom, $this->userId);
|
||||
|
||||
if (!$parentRoomParticipant->hasModeratorPermissions()) {
|
||||
return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
if ($parentRoom->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
|
||||
return new DataResponse(['error' => 'mode'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Overwriting the type with the parent type.
|
||||
$roomType = $parentRoom->getType();
|
||||
} catch (RoomNotFoundException $e) {
|
||||
return new DataResponse(['error' => 'room'], Http::STATUS_BAD_REQUEST);
|
||||
} catch (ParticipantNotFoundException $e) {
|
||||
return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
} elseif ($objectType === Room::OBJECT_TYPE_PHONE) {
|
||||
// Ignoring any user input on this one
|
||||
$objectId = $objectType;
|
||||
} elseif ($objectType === Room::OBJECT_TYPE_EVENT) {
|
||||
// Allow event rooms in future versions without breaking in older talk versions that the same calendar version supports
|
||||
$objectType = '';
|
||||
$objectId = '';
|
||||
} elseif ($objectType !== '') {
|
||||
return new DataResponse(['error' => 'object'], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Create the room
|
||||
try {
|
||||
$room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId, $password);
|
||||
} catch (PasswordException $e) {
|
||||
return new DataResponse(['error' => 'password', 'message' => $e->getHint()], Http::STATUS_BAD_REQUEST);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$currentParticipant = $this->participantService->getParticipant($room, $currentUser->getUID(), false);
|
||||
if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
|
||||
// Enforce the lobby state when breakout rooms are disabled
|
||||
if ($parentRoom instanceof Room && $parentRoom->getBreakoutRoomStatus() === BreakoutRoom::STATUS_STOPPED) {
|
||||
$this->roomService->setLobby($room, Webinary::LOBBY_NON_MODERATORS, null, false, false);
|
||||
}
|
||||
|
||||
$participants = $this->participantService->getParticipantsForRoom($parentRoom);
|
||||
$moderators = array_filter($participants, static function (Participant $participant) use ($currentParticipant) {
|
||||
return $participant->hasModeratorPermissions()
|
||||
&& $participant->getAttendee()->getId() !== $currentParticipant->getAttendee()->getId();
|
||||
});
|
||||
if (!empty($moderators)) {
|
||||
$this->breakoutRoomService->addModeratorsToBreakoutRooms([$room], $moderators);
|
||||
}
|
||||
}
|
||||
|
||||
return new DataResponse($this->formatRoom($room, $currentParticipant), Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a room to the favorites
|
||||
*
|
||||
|
|
@ -2304,7 +2300,7 @@ class RoomController extends AEnvironmentAwareOCSController {
|
|||
/**
|
||||
* Update the lobby state for a room
|
||||
*
|
||||
* @param int $state New state
|
||||
* @param 0|1 $state New state
|
||||
* @psalm-param Webinary::LOBBY_* $state
|
||||
* @param int|null $timer Timer when the lobby will be removed
|
||||
* @psalm-param non-negative-int|null $timer
|
||||
|
|
|
|||
44
lib/Exceptions/RoomProperty/CreationException.php
Normal file
44
lib/Exceptions/RoomProperty/CreationException.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Exceptions\RoomProperty;
|
||||
|
||||
class CreationException extends \InvalidArgumentException {
|
||||
public const REASON_AVATAR = 'avatar';
|
||||
public const REASON_DESCRIPTION = 'description';
|
||||
public const REASON_LISTABLE = 'listable';
|
||||
public const REASON_LOBBY = 'lobby';
|
||||
public const REASON_LOBBY_TIMER = 'lobby-timer';
|
||||
public const REASON_MESSAGE_EXPIRATION = 'message-expiration';
|
||||
public const REASON_MENTION_PERMISSIONS = 'mention-permissions';
|
||||
public const REASON_NAME = 'name';
|
||||
public const REASON_OBJECT = 'object';
|
||||
public const REASON_OBJECT_ID = 'object-id';
|
||||
public const REASON_OBJECT_TYPE = 'object-type';
|
||||
public const REASON_PERMISSIONS = 'permissions';
|
||||
public const REASON_READ_ONLY = 'read-only';
|
||||
public const REASON_RECORDING_CONSENT = 'recording-consent';
|
||||
public const REASON_SIP_ENABLED = 'sip-enabled';
|
||||
public const REASON_TYPE = 'type';
|
||||
|
||||
/**
|
||||
* @param self::REASON_* $reason
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $reason,
|
||||
) {
|
||||
parent::__construct($reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self::REASON_*
|
||||
*/
|
||||
public function getReason(): string {
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
||||
|
|
@ -1102,16 +1102,32 @@ class Manager {
|
|||
return $room;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
* @param string $name
|
||||
* @param string $objectType
|
||||
* @param string $objectId
|
||||
* @param string $password
|
||||
* @return Room
|
||||
*/
|
||||
public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = '', string $password = ''): Room {
|
||||
public function createRoom(
|
||||
int $type,
|
||||
string $name = '',
|
||||
string $objectType = '',
|
||||
string $objectId = '',
|
||||
string $password = '',
|
||||
?int $readOnly = null,
|
||||
?int $listable = null,
|
||||
?int $messageExpiration = null,
|
||||
?int $lobbyState = null,
|
||||
?\DateTime $lobbyTimer = null,
|
||||
?int $sipEnabled = null,
|
||||
?int $permissions = null,
|
||||
?int $recordingConsent = null,
|
||||
?int $mentionPermissions = null,
|
||||
?string $description = null,
|
||||
): Room {
|
||||
$token = $this->getNewToken();
|
||||
$row = [
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'token' => $token,
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $objectId,
|
||||
'password' => $password,
|
||||
];
|
||||
|
||||
$insert = $this->db->getQueryBuilder();
|
||||
$insert->insert('talk_rooms')
|
||||
|
|
@ -1129,17 +1145,50 @@ class Manager {
|
|||
->setValue('object_id', $insert->createNamedParameter($objectId));
|
||||
}
|
||||
|
||||
if ($readOnly !== null) {
|
||||
$insert->setValue('read_only', $insert->createNamedParameter($readOnly, IQueryBuilder::PARAM_INT));
|
||||
$row['read_only'] = $readOnly;
|
||||
}
|
||||
if ($listable !== null) {
|
||||
$insert->setValue('listable', $insert->createNamedParameter($listable, IQueryBuilder::PARAM_INT));
|
||||
$row['listable'] = $listable;
|
||||
}
|
||||
if ($messageExpiration !== null) {
|
||||
$insert->setValue('message_expiration', $insert->createNamedParameter($messageExpiration, IQueryBuilder::PARAM_INT));
|
||||
$row['message_expiration'] = $messageExpiration;
|
||||
}
|
||||
if ($lobbyState !== null) {
|
||||
$insert->setValue('lobby_state', $insert->createNamedParameter($lobbyState, IQueryBuilder::PARAM_INT));
|
||||
$row['lobby_state'] = $lobbyState;
|
||||
if ($lobbyTimer !== null) {
|
||||
$insert->setValue('lobby_timer', $insert->createNamedParameter($lobbyTimer, IQueryBuilder::PARAM_DATETIME_MUTABLE));
|
||||
$row['lobby_timer'] = $lobbyTimer->format(\DATE_ATOM);
|
||||
}
|
||||
}
|
||||
if ($sipEnabled !== null) {
|
||||
$insert->setValue('sip_enabled', $insert->createNamedParameter($sipEnabled, IQueryBuilder::PARAM_INT));
|
||||
$row['sip_enabled'] = $sipEnabled;
|
||||
}
|
||||
if ($permissions !== null) {
|
||||
$insert->setValue('default_permissions', $insert->createNamedParameter($permissions, IQueryBuilder::PARAM_INT));
|
||||
$row['default_permissions'] = $permissions;
|
||||
}
|
||||
if ($recordingConsent !== null) {
|
||||
$insert->setValue('recording_consent', $insert->createNamedParameter($recordingConsent, IQueryBuilder::PARAM_INT));
|
||||
$row['recording_consent'] = $recordingConsent;
|
||||
}
|
||||
if ($mentionPermissions !== null) {
|
||||
$insert->setValue('mention_permissions', $insert->createNamedParameter($mentionPermissions, IQueryBuilder::PARAM_INT));
|
||||
$row['mention_permissions'] = $mentionPermissions;
|
||||
}
|
||||
if ($description !== null) {
|
||||
$insert->setValue('description', $insert->createNamedParameter($description));
|
||||
$row['description'] = $description;
|
||||
}
|
||||
|
||||
$insert->executeStatement();
|
||||
$roomId = $insert->getLastInsertId();
|
||||
$room = $this->createRoomObjectFromData([
|
||||
'r_id' => $roomId,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'token' => $token,
|
||||
'object_type' => $objectType,
|
||||
'object_id' => $objectId,
|
||||
'password' => $password
|
||||
]);
|
||||
$row['r_id'] = $insert->getLastInsertId();
|
||||
$room = $this->createRoomObjectFromData($row);
|
||||
|
||||
$event = new RoomCreatedEvent($room);
|
||||
$this->dispatcher->dispatchTyped($event);
|
||||
|
|
|
|||
177
lib/Model/InvitationList.php
Normal file
177
lib/Model/InvitationList.php
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Model;
|
||||
|
||||
use OCA\Circles\Model\Circle;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\IGroup;
|
||||
use OCP\IUser;
|
||||
|
||||
class InvitationList {
|
||||
|
||||
/** @var array<string, IUser> */
|
||||
protected array $validUsers = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidUsers = [];
|
||||
|
||||
/** @var array<string, ICloudId> */
|
||||
protected array $validFederatedUsers = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidFederatedUsers = [];
|
||||
|
||||
/** @var array<string, IGroup> */
|
||||
protected array $validGroups = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidGroups = [];
|
||||
|
||||
/** @var array<string, Circle> */
|
||||
protected array $validTeams = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidTeams = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected array $validEmails = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidEmails = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
protected array $validPhoneNumbers = [];
|
||||
/** @var list<string> */
|
||||
protected array $invalidPhoneNumbers = [];
|
||||
|
||||
/**
|
||||
* @param array<string, IUser> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setUserResults(array $valid, array $invalid): void {
|
||||
$this->validUsers = $valid;
|
||||
$this->invalidUsers = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ICloudId> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setFederatedUserResults(array $valid, array $invalid): void {
|
||||
$this->validFederatedUsers = $valid;
|
||||
$this->invalidFederatedUsers = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, IGroup> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setGroupResults(array $valid, array $invalid): void {
|
||||
$this->validGroups = $valid;
|
||||
$this->invalidGroups = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Circle> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setTeamResults(array $valid, array $invalid): void {
|
||||
$this->validTeams = $valid;
|
||||
$this->invalidTeams = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setEmailResults(array $valid, array $invalid): void {
|
||||
$this->validEmails = $valid;
|
||||
$this->invalidEmails = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $valid
|
||||
* @param list<string> $invalid
|
||||
*/
|
||||
public function setPhoneNumberResults(array $valid, array $invalid): void {
|
||||
$this->validPhoneNumbers = $valid;
|
||||
$this->invalidPhoneNumbers = $invalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, IUser>
|
||||
*/
|
||||
public function getUsers(): array {
|
||||
return $this->validUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, ICloudId>
|
||||
*/
|
||||
public function getFederatedUsers(): array {
|
||||
return $this->validFederatedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, IGroup>
|
||||
*/
|
||||
public function getGroup(): array {
|
||||
return $this->validGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Circle>
|
||||
*/
|
||||
public function getTeams(): array {
|
||||
return $this->validTeams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getEmails(): array {
|
||||
return $this->validEmails;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getPhoneNumbers(): array {
|
||||
return $this->validPhoneNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<'users'|'federated_users'|'groups'|'emails'|'phones'|'teams', list<string>>
|
||||
*/
|
||||
public function getInvalidList(): array {
|
||||
$response = [
|
||||
'users' => $this->invalidUsers,
|
||||
'federated_users' => $this->invalidFederatedUsers,
|
||||
'groups' => $this->invalidGroups,
|
||||
'teams' => $this->invalidTeams,
|
||||
'emails' => $this->invalidEmails,
|
||||
'phones' => $this->invalidPhoneNumbers,
|
||||
];
|
||||
return array_filter($response);
|
||||
}
|
||||
|
||||
public function hasValidInvitations(): bool {
|
||||
return !empty($this->validUsers)
|
||||
|| !empty($this->validFederatedUsers)
|
||||
|| !empty($this->validGroups)
|
||||
|| !empty($this->validTeams)
|
||||
|| !empty($this->validEmails)
|
||||
|| !empty($this->validPhoneNumbers);
|
||||
}
|
||||
|
||||
public function hasInvalidInvitations(): bool {
|
||||
return !empty($this->invalidUsers)
|
||||
|| !empty($this->invalidFederatedUsers)
|
||||
|| !empty($this->invalidGroups)
|
||||
|| !empty($this->invalidTeams)
|
||||
|| !empty($this->invalidEmails)
|
||||
|| !empty($this->invalidPhoneNumbers);
|
||||
}
|
||||
}
|
||||
|
|
@ -222,6 +222,15 @@ namespace OCA\Talk;
|
|||
* timestamp: int,
|
||||
* }
|
||||
*
|
||||
* @psalm-type TalkInvitationList = array{
|
||||
* users?: list<string>,
|
||||
* federated_users?: list<string>,
|
||||
* groups?: list<string>,
|
||||
* emails?: list<string>,
|
||||
* phones?: list<string>,
|
||||
* teams?: list<string>,
|
||||
* }
|
||||
*
|
||||
* @psalm-type TalkRoom = array{
|
||||
* actorId: string,
|
||||
* invitedActorId?: string,
|
||||
|
|
@ -284,6 +293,10 @@ namespace OCA\Talk;
|
|||
* isArchived: bool,
|
||||
* }
|
||||
*
|
||||
* @psalm-type TalkRoomWithInvalidInvitations = TalkRoom&array{
|
||||
* invalidParticipants: TalkInvitationList,
|
||||
* }
|
||||
*
|
||||
* @psalm-type TalkSignalingSession = array{
|
||||
* actorId: string,
|
||||
* actorType: string,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ use OCP\Files\SimpleFS\InMemoryFile;
|
|||
use OCP\Files\SimpleFS\ISimpleFile;
|
||||
use OCP\Files\SimpleFS\ISimpleFolder;
|
||||
use OCP\IAvatarManager;
|
||||
use OCP\IEmojiHelper;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
|
|
@ -36,7 +35,7 @@ class AvatarService {
|
|||
private ISecureRandom $random,
|
||||
private RoomService $roomService,
|
||||
private IAvatarManager $avatarManager,
|
||||
private IEmojiHelper $emojiHelper,
|
||||
private EmojiService $emojiService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -73,7 +72,7 @@ class AvatarService {
|
|||
throw new InvalidArgumentException($this->l->t('One-to-one rooms always need to show the other users avatar'));
|
||||
}
|
||||
|
||||
if ($this->getFirstCombinedEmoji($emoji) !== $emoji) {
|
||||
if ($this->emojiService->getFirstCombinedEmoji($emoji) !== $emoji) {
|
||||
throw new InvalidArgumentException($this->l->t('Invalid emoji character'));
|
||||
}
|
||||
|
||||
|
|
@ -207,12 +206,11 @@ class AvatarService {
|
|||
}
|
||||
}
|
||||
}
|
||||
if ($this->emojiHelper->doesPlatformSupportEmoji() && $this->emojiHelper->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) {
|
||||
if ($this->emojiService->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) {
|
||||
return new InMemoryFile(
|
||||
$token,
|
||||
$this->getEmojiAvatar(
|
||||
$this->getFirstCombinedEmoji(
|
||||
$room->getName()),
|
||||
$this->emojiService->getFirstCombinedEmoji($room->getName()),
|
||||
$darkTheme ? self::THEMING_DARK_BACKGROUND : self::THEMING_BRIGHT_BACKGROUND
|
||||
)
|
||||
);
|
||||
|
|
@ -251,26 +249,6 @@ class AvatarService {
|
|||
], $this->svgTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first combined full emoji (including gender, skin tone, job, …)
|
||||
*
|
||||
* @param string $roomName
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
protected function getFirstCombinedEmoji(string $roomName, int $length = 0): string {
|
||||
if (!$this->emojiHelper->doesPlatformSupportEmoji() || mb_strlen($roomName) === $length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attempt = mb_substr($roomName, 0, $length + 1);
|
||||
if ($this->emojiHelper->isValidSingleEmoji($attempt)) {
|
||||
$longerAttempt = $this->getFirstCombinedEmoji($roomName, $length + 1);
|
||||
return $longerAttempt ?: $attempt;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isCustomAvatar(Room $room): bool {
|
||||
return $room->getAvatar() !== '';
|
||||
}
|
||||
|
|
@ -335,8 +313,8 @@ class AvatarService {
|
|||
[$version] = explode('.', $avatarVersion);
|
||||
return $version;
|
||||
}
|
||||
if ($this->emojiHelper->doesPlatformSupportEmoji() && $this->emojiHelper->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) {
|
||||
return substr(md5($this->getEmojiAvatar($this->getFirstCombinedEmoji($room->getName()), self::THEMING_BRIGHT_BACKGROUND)), 0, 8);
|
||||
if ($this->emojiService->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) {
|
||||
return substr(md5($this->getEmojiAvatar($this->emojiService->getFirstCombinedEmoji($room->getName()), self::THEMING_BRIGHT_BACKGROUND)), 0, 8);
|
||||
}
|
||||
$avatarPath = $this->getAvatarPath($room);
|
||||
return substr(md5($avatarPath), 0, 8);
|
||||
|
|
|
|||
43
lib/Service/EmojiService.php
Normal file
43
lib/Service/EmojiService.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Service;
|
||||
|
||||
use OCP\IEmojiHelper;
|
||||
|
||||
class EmojiService {
|
||||
|
||||
public function __construct(
|
||||
protected IEmojiHelper $emojiHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first combined full emoji (including gender, skin tone, job, …)
|
||||
*
|
||||
* @param string $roomName
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstCombinedEmoji(string $roomName, int $length = 0): string {
|
||||
if (!$this->emojiHelper->doesPlatformSupportEmoji() || mb_strlen($roomName) === $length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attempt = mb_substr($roomName, 0, $length + 1);
|
||||
if ($this->emojiHelper->isValidSingleEmoji($attempt)) {
|
||||
$longerAttempt = $this->getFirstCombinedEmoji($roomName, $length + 1);
|
||||
return $longerAttempt ?: $attempt;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function isValidSingleEmoji(string $string): bool {
|
||||
return $this->emojiHelper->doesPlatformSupportEmoji() && $this->emojiHelper->isValidSingleEmoji(mb_substr($string, 0, 1));
|
||||
}
|
||||
}
|
||||
199
lib/Service/InvitationService.php
Normal file
199
lib/Service/InvitationService.php
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Service;
|
||||
|
||||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Exceptions\FederationRestrictionException;
|
||||
use OCA\Talk\Federation\FederationManager;
|
||||
use OCA\Talk\MatterbridgeManager;
|
||||
use OCA\Talk\Model\InvitationList;
|
||||
use OCA\Talk\Room;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IPhoneNumberUtil;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Mail\IMailer;
|
||||
|
||||
class InvitationService {
|
||||
public function __construct(
|
||||
protected IAppManager $appManager,
|
||||
protected ICloudIdManager $cloudIdManager,
|
||||
protected IGroupManager $groupManager,
|
||||
protected IPhoneNumberUtil $phoneNumberUtil,
|
||||
protected IUserManager $userManager,
|
||||
protected FederationManager $federationManager,
|
||||
protected ParticipantService $participantService,
|
||||
protected IConfig $serverConfig,
|
||||
protected Config $talkConfig,
|
||||
protected IMailer $mailer,
|
||||
) {
|
||||
}
|
||||
|
||||
public function validateInvitations(array $participants, IUser $currentUser, ?Room $room = null): InvitationList {
|
||||
$invitationList = new InvitationList();
|
||||
if (!empty($participants['users'])) {
|
||||
$this->validateUserInvitations($invitationList, $participants['users']);
|
||||
}
|
||||
if (!empty($participants['emails'])) {
|
||||
$this->validateEmailInvitations($invitationList, $participants['emails']);
|
||||
}
|
||||
if (!empty($participants['groups'])) {
|
||||
$this->validateGroupInvitations($invitationList, $participants['groups']);
|
||||
}
|
||||
if (!empty($participants['teams'])) {
|
||||
$this->validateTeamInvitations($invitationList, $participants['teams'], $currentUser);
|
||||
}
|
||||
if (!empty($participants['federated_users'])) {
|
||||
$this->validateFederatedUserInvitations($invitationList, $participants['federated_users'], $currentUser);
|
||||
}
|
||||
if (!empty($participants['phones'])) {
|
||||
$this->validatePhoneInvitations($invitationList, $participants['phones'], $currentUser, $room);
|
||||
}
|
||||
return $invitationList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $userIds
|
||||
*/
|
||||
protected function validateUserInvitations(InvitationList $invitationList, array $userIds): void {
|
||||
$invalidUsers = $validUsers = [];
|
||||
foreach ($userIds as $userId) {
|
||||
if ($userId === MatterbridgeManager::BRIDGE_BOT_USERID) {
|
||||
$invalidUsers[] = $userId;
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user instanceof IUser) {
|
||||
$validUsers[$userId] = $user;
|
||||
} else {
|
||||
$invalidUsers[] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
$invitationList->setUserResults($validUsers, $invalidUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $emails
|
||||
*/
|
||||
protected function validateEmailInvitations(InvitationList $invitationList, array $emails): void {
|
||||
$invalidEmails = $validEmails = [];
|
||||
foreach ($emails as $email) {
|
||||
if ($this->mailer->validateMailAddress($email)) {
|
||||
$validEmails[$email] = strtolower($email);
|
||||
} else {
|
||||
$invalidEmails[] = $email;
|
||||
}
|
||||
}
|
||||
$invitationList->setEmailResults($validEmails, $invalidEmails);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $groupIds
|
||||
*/
|
||||
protected function validateGroupInvitations(InvitationList $invitationList, array $groupIds): void {
|
||||
$invalidGroups = $validGroups = [];
|
||||
|
||||
foreach ($groupIds as $groupId) {
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group instanceof IGroup) {
|
||||
$validGroups[$groupId] = $group;
|
||||
} else {
|
||||
$invalidGroups[] = $groupId;
|
||||
}
|
||||
}
|
||||
$invitationList->setGroupResults($validGroups, $invalidGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $teamIds
|
||||
*/
|
||||
protected function validateTeamInvitations(InvitationList $invitationList, array $teamIds, IUser $currentUser): void {
|
||||
if (!$this->appManager->isEnabledForUser('circles')) {
|
||||
$invitationList->setTeamResults([], $teamIds);
|
||||
return;
|
||||
}
|
||||
|
||||
$invalidTeams = $validTeams = [];
|
||||
|
||||
foreach ($teamIds as $teamId) {
|
||||
try {
|
||||
$team = $this->participantService->getCircle($teamId, $currentUser->getUID());
|
||||
$validTeams[$teamId] = $team;
|
||||
} catch (\Exception) {
|
||||
$invalidTeams[] = $teamId;
|
||||
}
|
||||
}
|
||||
|
||||
$invitationList->setTeamResults($validTeams, $invalidTeams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $cloudIds
|
||||
*/
|
||||
protected function validateFederatedUserInvitations(InvitationList $invitationList, array $cloudIds, IUser $currentUser): void {
|
||||
if (!$this->talkConfig->isFederationEnabled()) {
|
||||
$invitationList->setFederatedUserResults([], $cloudIds);
|
||||
return;
|
||||
}
|
||||
|
||||
$invalidCloudIds = $validCloudIds = [];
|
||||
foreach ($cloudIds as $cloudIdString) {
|
||||
try {
|
||||
$cloudId = $this->cloudIdManager->resolveCloudId($cloudIdString);
|
||||
$this->federationManager->isAllowedToInvite($currentUser, $cloudId);
|
||||
$validCloudIds[$cloudIdString] = $cloudId;
|
||||
} catch (\InvalidArgumentException|FederationRestrictionException) {
|
||||
$invalidCloudIds[] = $cloudIdString;
|
||||
}
|
||||
}
|
||||
$invitationList->setFederatedUserResults($validCloudIds, $invalidCloudIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $phoneNumbers
|
||||
*/
|
||||
protected function validatePhoneInvitations(InvitationList $invitationList, array $phoneNumbers, IUser $currentUser, ?Room $room): void {
|
||||
if (!$this->talkConfig->isSIPConfigured() || !$this->talkConfig->canUserDialOutSIP($currentUser)) {
|
||||
$invitationList->setPhoneNumberResults([], $phoneNumbers);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($room instanceof Room
|
||||
&& (preg_match(Room::SIP_INCOMPATIBLE_REGEX, $room->getToken())
|
||||
|| !in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true))) {
|
||||
$invitationList->setPhoneNumberResults([], $phoneNumbers);
|
||||
return;
|
||||
}
|
||||
|
||||
$phoneRegion = $this->serverConfig->getSystemValueString('default_phone_region');
|
||||
if ($phoneRegion === '') {
|
||||
$phoneRegion = null;
|
||||
}
|
||||
|
||||
$invalidPhoneNumbers = [];
|
||||
$validPhoneNumbers = [];
|
||||
|
||||
foreach ($phoneNumbers as $phoneNumber) {
|
||||
$formattedNumber = $this->phoneNumberUtil->convertToStandardFormat($phoneNumber, $phoneRegion);
|
||||
if ($formattedNumber === null) {
|
||||
$invalidPhoneNumbers[] = $phoneNumber;
|
||||
} else {
|
||||
$validPhoneNumbers[$phoneNumber] = $formattedNumber;
|
||||
}
|
||||
}
|
||||
|
||||
$invitationList->setPhoneNumberResults($validPhoneNumbers, $invalidPhoneNumbers);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,10 +46,12 @@ use OCA\Talk\Exceptions\ParticipantProperty\PermissionsException;
|
|||
use OCA\Talk\Exceptions\UnauthorizedException;
|
||||
use OCA\Talk\Federation\BackendNotifier;
|
||||
use OCA\Talk\Federation\FederationManager;
|
||||
use OCA\Talk\GuestManager;
|
||||
use OCA\Talk\Manager;
|
||||
use OCA\Talk\Model\Attendee;
|
||||
use OCA\Talk\Model\AttendeeMapper;
|
||||
use OCA\Talk\Model\BreakoutRoom;
|
||||
use OCA\Talk\Model\InvitationList;
|
||||
use OCA\Talk\Model\SelectHelper;
|
||||
use OCA\Talk\Model\Session;
|
||||
use OCA\Talk\Model\SessionMapper;
|
||||
|
|
@ -474,6 +476,65 @@ class ParticipantService {
|
|||
return $participant;
|
||||
}
|
||||
|
||||
public function addInvitationList(Room $room, InvitationList $invitationList, ?IUser $addedBy = null): void {
|
||||
$participantsToAdd = [];
|
||||
foreach ($invitationList->getUsers() as $user) {
|
||||
$participantsToAdd[] = [
|
||||
'actorType' => Attendee::ACTOR_USERS,
|
||||
'actorId' => $user->getUID(),
|
||||
'displayName' => $user->getDisplayName(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($invitationList->getFederatedUsers() as $cloudId) {
|
||||
$participantsToAdd[] = [
|
||||
'actorType' => Attendee::ACTOR_FEDERATED_USERS,
|
||||
'actorId' => $cloudId->getId(),
|
||||
'displayName' => $cloudId->getDisplayId(),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($invitationList->getPhoneNumbers() as $phoneNumber) {
|
||||
$participantsToAdd[] = [
|
||||
'actorType' => Attendee::ACTOR_PHONES,
|
||||
'actorId' => sha1($phoneNumber . '#' . $this->timeFactory->getTime()),
|
||||
'displayName' => substr($phoneNumber, 0, -4) . '…', // FIXME Allow the UI to hand in a name (when selected from contacts?)
|
||||
'phoneNumber' => $phoneNumber,
|
||||
];
|
||||
}
|
||||
|
||||
$existingParticipants = [];
|
||||
if (!empty($participantsToAdd)) {
|
||||
$attendees = $this->addUsers($room, $participantsToAdd, $addedBy);
|
||||
$existingParticipants = array_map(static fn (Attendee $attendee): Participant => new Participant($room, $attendee, null), $attendees);
|
||||
}
|
||||
|
||||
$emails = $invitationList->getEmails();
|
||||
if (!empty($emails)) {
|
||||
$guestManager = Server::get(GuestManager::class);
|
||||
foreach ($emails as $email) {
|
||||
$actorId = hash('sha256', $email);
|
||||
try {
|
||||
$this->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $actorId);
|
||||
} catch (ParticipantNotFoundException) {
|
||||
$participant = $this->inviteEmailAddress($room, $actorId, $email);
|
||||
try {
|
||||
$guestManager->sendEmailInvitation($room, $participant);
|
||||
} catch (\InvalidArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($invitationList->getGroup() as $group) {
|
||||
$this->addGroup($room, $group, $existingParticipants);
|
||||
}
|
||||
|
||||
foreach ($invitationList->getTeams() as $team) {
|
||||
$this->addCircle($room, $team, $existingParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Room $room
|
||||
* @param array $participants
|
||||
|
|
@ -640,9 +701,9 @@ class ParticipantService {
|
|||
/**
|
||||
* @param Room $room
|
||||
* @param IGroup $group
|
||||
* @param Participant[] $existingParticipants
|
||||
* @param Participant[] &$existingParticipants
|
||||
*/
|
||||
public function addGroup(Room $room, IGroup $group, array $existingParticipants = []): void {
|
||||
public function addGroup(Room $room, IGroup $group, array &$existingParticipants = []): void {
|
||||
$usersInGroup = $group->getUsers();
|
||||
|
||||
if (empty($existingParticipants)) {
|
||||
|
|
@ -699,7 +760,10 @@ class ParticipantService {
|
|||
$this->dispatcher->dispatchTyped($attendeeEvent);
|
||||
}
|
||||
|
||||
$this->addUsers($room, $newParticipants, bansAlreadyChecked: true);
|
||||
$attendees = $this->addUsers($room, $newParticipants, bansAlreadyChecked: true);
|
||||
if (!empty($attendees)) {
|
||||
$existingParticipants = array_merge(array_map(static fn (Attendee $attendee): Participant => new Participant($room, $attendee, null), $attendees), $existingParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -759,9 +823,9 @@ class ParticipantService {
|
|||
/**
|
||||
* @param Room $room
|
||||
* @param Circle $circle
|
||||
* @param Participant[] $existingParticipants
|
||||
* @param Participant[] &$existingParticipants
|
||||
*/
|
||||
public function addCircle(Room $room, Circle $circle, array $existingParticipants = []): void {
|
||||
public function addCircle(Room $room, Circle $circle, array &$existingParticipants = []): void {
|
||||
$membersInCircle = $circle->getInheritedMembers();
|
||||
|
||||
if (empty($existingParticipants)) {
|
||||
|
|
@ -834,7 +898,10 @@ class ParticipantService {
|
|||
$this->dispatcher->dispatchTyped($attendeeEvent);
|
||||
}
|
||||
|
||||
$this->addUsers($room, $newParticipants, bansAlreadyChecked: true);
|
||||
$attendees = $this->addUsers($room, $newParticipants, bansAlreadyChecked: true);
|
||||
if (!empty($attendees)) {
|
||||
$existingParticipants = array_merge(array_map(static fn (Attendee $attendee): Participant => new Participant($room, $attendee, null), $attendees), $existingParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public function inviteEmailAddress(Room $room, string $actorId, string $email, ?string $name = null): Participant {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ use OCA\Talk\Exceptions\RoomProperty\AvatarException;
|
|||
use OCA\Talk\Exceptions\RoomProperty\BreakoutRoomModeException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\BreakoutRoomStatusException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\CallRecordingException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\CreationException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\DefaultPermissionsException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\DescriptionException;
|
||||
use OCA\Talk\Exceptions\RoomProperty\ListableException;
|
||||
|
|
@ -62,6 +63,7 @@ use OCP\IUser;
|
|||
use OCP\Log\Audit\CriticalActionPerformedEvent;
|
||||
use OCP\Security\Events\ValidatePasswordPolicyEvent;
|
||||
use OCP\Security\IHasher;
|
||||
use OCP\Server;
|
||||
use OCP\Share\IManager as IShareManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
|
|
@ -80,6 +82,7 @@ class RoomService {
|
|||
protected IHasher $hasher,
|
||||
protected IEventDispatcher $dispatcher,
|
||||
protected IJobList $jobList,
|
||||
protected EmojiService $emojiService,
|
||||
protected LoggerInterface $logger,
|
||||
protected IL10N $l10n,
|
||||
) {
|
||||
|
|
@ -130,47 +133,140 @@ class RoomService {
|
|||
|
||||
/**
|
||||
* @return Room
|
||||
* @throws InvalidArgumentException on too long or empty names
|
||||
* @throws InvalidArgumentException unsupported type
|
||||
* @throws InvalidArgumentException invalid object data
|
||||
* @throws CreationException
|
||||
* @throws PasswordException empty or invalid password
|
||||
*/
|
||||
public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = '', string $password = ''): Room {
|
||||
public function createConversation(
|
||||
int $type,
|
||||
string $name,
|
||||
?IUser $owner = null,
|
||||
string $objectType = '',
|
||||
string $objectId = '',
|
||||
string $password = '',
|
||||
int $readOnly = Room::READ_WRITE,
|
||||
int $listable = Room::LISTABLE_NONE,
|
||||
int $messageExpiration = 0,
|
||||
int $lobbyState = Webinary::LOBBY_NONE,
|
||||
?int $lobbyTimer = null,
|
||||
int $sipEnabled = Webinary::SIP_DISABLED,
|
||||
int $permissions = Attendee::PERMISSIONS_DEFAULT,
|
||||
int $recordingConsent = RecordingService::CONSENT_REQUIRED_NO,
|
||||
int $mentionPermissions = Room::MENTION_PERMISSIONS_EVERYONE,
|
||||
string $description = '',
|
||||
?string $emoji = null,
|
||||
?string $avatarColor = null,
|
||||
bool $allowInternalTypes = true,
|
||||
): Room {
|
||||
$name = trim($name);
|
||||
if ($name === '' || mb_strlen($name) > 255) {
|
||||
throw new InvalidArgumentException('name');
|
||||
throw new CreationException(CreationException::REASON_NAME);
|
||||
}
|
||||
|
||||
if (!\in_array($type, [
|
||||
$types = [
|
||||
Room::TYPE_GROUP,
|
||||
Room::TYPE_PUBLIC,
|
||||
Room::TYPE_CHANGELOG,
|
||||
Room::TYPE_NOTE_TO_SELF,
|
||||
], true)) {
|
||||
throw new InvalidArgumentException('type');
|
||||
];
|
||||
if ($allowInternalTypes) {
|
||||
$types[] = Room::TYPE_CHANGELOG;
|
||||
$types[] = Room::TYPE_NOTE_TO_SELF;
|
||||
}
|
||||
if (!\in_array($type, $types, true)) {
|
||||
throw new CreationException(CreationException::REASON_TYPE);
|
||||
}
|
||||
|
||||
$objectType = trim($objectType);
|
||||
if (isset($objectType[64])) {
|
||||
throw new InvalidArgumentException('object_type');
|
||||
throw new CreationException(CreationException::REASON_OBJECT_TYPE);
|
||||
}
|
||||
|
||||
$objectId = trim($objectId);
|
||||
if (isset($objectId[64])) {
|
||||
throw new InvalidArgumentException('object_id');
|
||||
throw new CreationException(CreationException::REASON_OBJECT_ID);
|
||||
}
|
||||
|
||||
$objectTypes = [
|
||||
'',
|
||||
Room::OBJECT_TYPE_PHONE,
|
||||
Room::OBJECT_TYPE_EVENT,
|
||||
];
|
||||
if ($allowInternalTypes) {
|
||||
$objectTypes[] = Room::OBJECT_TYPE_EMAIL;
|
||||
$objectTypes[] = Room::OBJECT_TYPE_FILE;
|
||||
$objectTypes[] = Room::OBJECT_TYPE_SAMPLE;
|
||||
$objectTypes[] = Room::OBJECT_TYPE_VIDEO_VERIFICATION;
|
||||
}
|
||||
|
||||
if (!in_array($objectType, $objectTypes, true)) {
|
||||
throw new CreationException(CreationException::REASON_OBJECT_TYPE);
|
||||
}
|
||||
|
||||
if (($objectType !== '' && $objectId === '') ||
|
||||
($objectType === '' && $objectId !== '')) {
|
||||
throw new InvalidArgumentException('object');
|
||||
throw new CreationException(CreationException::REASON_OBJECT);
|
||||
}
|
||||
|
||||
if ($type === Room::TYPE_PUBLIC && $password === '' && $this->config->isPasswordEnforced()) {
|
||||
throw new PasswordException(PasswordException::REASON_VALUE, $this->l10n->t('Password needs to be set'));
|
||||
}
|
||||
|
||||
if (!in_array($readOnly, [Room::READ_WRITE, Room::READ_ONLY], true)) {
|
||||
throw new CreationException(CreationException::REASON_READ_ONLY);
|
||||
}
|
||||
|
||||
if (!in_array($listable, [Room::LISTABLE_NONE, Room::LISTABLE_USERS, Room::LISTABLE_ALL], true)) {
|
||||
throw new CreationException(CreationException::REASON_LISTABLE);
|
||||
}
|
||||
|
||||
if ($messageExpiration < 0) {
|
||||
throw new CreationException(CreationException::REASON_MESSAGE_EXPIRATION);
|
||||
}
|
||||
|
||||
if (!in_array($lobbyState, [Webinary::LOBBY_NONE, Webinary::LOBBY_NON_MODERATORS], true)) {
|
||||
throw new CreationException(CreationException::REASON_LOBBY);
|
||||
}
|
||||
|
||||
$lobbyTimerDateTime = null;
|
||||
if ($lobbyState !== Webinary::LOBBY_NONE && $lobbyTimer !== null) {
|
||||
if ($lobbyTimer < 0) {
|
||||
throw new CreationException(CreationException::REASON_LOBBY_TIMER);
|
||||
}
|
||||
|
||||
$lobbyTimerDateTime = $this->timeFactory->getDateTime('@' . $lobbyTimer);
|
||||
$lobbyTimerDateTime->setTimezone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
if (!in_array($sipEnabled, [Webinary::SIP_DISABLED, Webinary::SIP_ENABLED, Webinary::SIP_ENABLED_NO_PIN], true)) {
|
||||
throw new CreationException(CreationException::REASON_SIP_ENABLED);
|
||||
}
|
||||
|
||||
if ($permissions < Attendee::PERMISSIONS_DEFAULT || $permissions > Attendee::PERMISSIONS_MAX_CUSTOM) {
|
||||
throw new CreationException(CreationException::REASON_PERMISSIONS);
|
||||
} elseif ($permissions !== Attendee::PERMISSIONS_DEFAULT) {
|
||||
$permissions |= Attendee::PERMISSIONS_CUSTOM;
|
||||
}
|
||||
|
||||
if (!in_array($recordingConsent, [RecordingService::CONSENT_REQUIRED_NO, RecordingService::CONSENT_REQUIRED_YES], true)) {
|
||||
throw new CreationException(CreationException::REASON_RECORDING_CONSENT);
|
||||
}
|
||||
|
||||
if (!in_array($mentionPermissions, [Room::MENTION_PERMISSIONS_EVERYONE, Room::MENTION_PERMISSIONS_MODERATORS], true)) {
|
||||
throw new CreationException(CreationException::REASON_MENTION_PERMISSIONS);
|
||||
}
|
||||
if (mb_strlen($description) > Room::DESCRIPTION_MAXIMUM_LENGTH) {
|
||||
throw new CreationException(CreationException::REASON_DESCRIPTION);
|
||||
}
|
||||
|
||||
if ($emoji !== null) {
|
||||
if ($this->emojiService->getFirstCombinedEmoji($emoji) !== $emoji) {
|
||||
throw new CreationException(CreationException::REASON_AVATAR);
|
||||
}
|
||||
if ($avatarColor !== null && !preg_match('/^[a-fA-F0-9]{6}$/', $avatarColor)) {
|
||||
throw new CreationException(CreationException::REASON_AVATAR);
|
||||
}
|
||||
}
|
||||
|
||||
if ($type !== Room::TYPE_PUBLIC || $password === '') {
|
||||
$room = $this->manager->createRoom($type, $name, $objectType, $objectId);
|
||||
$passwordHash = '';
|
||||
} else {
|
||||
$event = new ValidatePasswordPolicyEvent($password);
|
||||
try {
|
||||
|
|
@ -179,7 +275,29 @@ class RoomService {
|
|||
throw new PasswordException(PasswordException::REASON_VALUE, $e->getHint());
|
||||
}
|
||||
$passwordHash = $this->hasher->hash($password);
|
||||
$room = $this->manager->createRoom($type, $name, $objectType, $objectId, $passwordHash);
|
||||
}
|
||||
|
||||
$room = $this->manager->createRoom(
|
||||
$type,
|
||||
$name,
|
||||
$objectType,
|
||||
$objectId,
|
||||
$passwordHash,
|
||||
$readOnly,
|
||||
$listable,
|
||||
$messageExpiration,
|
||||
$lobbyState,
|
||||
$lobbyTimerDateTime,
|
||||
$sipEnabled,
|
||||
$permissions,
|
||||
$recordingConsent,
|
||||
$mentionPermissions,
|
||||
$description,
|
||||
);
|
||||
|
||||
if ($emoji !== null) {
|
||||
$avatarService = Server::get(AvatarService::class);
|
||||
$avatarService->setAvatarFromEmoji($room, $emoji, $avatarColor);
|
||||
}
|
||||
|
||||
if ($owner instanceof IUser) {
|
||||
|
|
@ -1309,5 +1427,4 @@ class RoomService {
|
|||
throw new TypeException(TypeException::REASON_BREAKOUT_ROOM);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
<referencedClass name="OC\DB\ConnectionAdapter" />
|
||||
<referencedClass name="OC\User\NoUserException" />
|
||||
<referencedClass name="OCA\Circles\CirclesManager" />
|
||||
<referencedClass name="OCA\Circles\Model\Circle" />
|
||||
<referencedClass name="OCA\Circles\Model\Member" />
|
||||
<referencedClass name="OCA\DAV\CardDAV\PhotoCache" />
|
||||
<referencedClass name="OCA\FederatedFileSharing\AddressHandler" />
|
||||
|
|
|
|||
|
|
@ -536,6 +536,15 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
if (isset($expectedRoom['lobbyState'])) {
|
||||
$data['lobbyState'] = (int)$room['lobbyState'];
|
||||
}
|
||||
if (!empty($expectedRoom['lobbyTimer'])) {
|
||||
$data['lobbyTimer'] = (int)$room['lobbyTimer'];
|
||||
}
|
||||
if (isset($expectedRoom['lobbyTimer'])) {
|
||||
$data['lobbyTimer'] = (int)$room['lobbyTimer'];
|
||||
if ($expectedRoom['lobbyTimer'] === 'GREATER_THAN_ZERO' && $room['lobbyTimer'] > 0) {
|
||||
$data['lobbyTimer'] = 'GREATER_THAN_ZERO';
|
||||
}
|
||||
}
|
||||
if (isset($expectedRoom['breakoutRoomMode'])) {
|
||||
$data['breakoutRoomMode'] = (int)$room['breakoutRoomMode'];
|
||||
}
|
||||
|
|
@ -593,6 +602,9 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
if (isset($expectedRoom['defaultPermissions'])) {
|
||||
$data['defaultPermissions'] = $this->mapPermissionsAPIOutput($room['defaultPermissions']);
|
||||
}
|
||||
if (isset($expectedRoom['mentionPermissions'])) {
|
||||
$data['mentionPermissions'] = (int)$room['mentionPermissions'];
|
||||
}
|
||||
if (isset($expectedRoom['participants'])) {
|
||||
throw new \Exception('participants key needs to be checked via participants endpoint');
|
||||
}
|
||||
|
|
@ -1158,6 +1170,14 @@ class FeatureContext implements Context, SnippetAcceptingContext {
|
|||
}
|
||||
}
|
||||
|
||||
if (isset($body['permissions'])) {
|
||||
$body['permissions'] = $this->mapPermissionsTestInput($body['permissions']);
|
||||
}
|
||||
if (isset($body['lobbyTimer'])) {
|
||||
if (preg_match('/^OFFSET\((\d+)\)$/', $body['lobbyTimer'], $matches)) {
|
||||
$body['lobbyTimer'] = $matches[1] + time();
|
||||
}
|
||||
}
|
||||
|
||||
$this->setCurrentUser($user);
|
||||
$this->sendRequest('POST', '/apps/spreed/api/' . $apiVersion . '/room', $body);
|
||||
|
|
|
|||
115
tests/integration/features/conversation-1/create.feature
Normal file
115
tests/integration/features/conversation-1/create.feature
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
Feature: conversation-1/create
|
||||
Background:
|
||||
Given user "participant1" exists
|
||||
Given user "participant2" exists
|
||||
|
||||
Scenario: Set password during creation
|
||||
Given user "participant1" creates room "room1" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room1 |
|
||||
| password | P4$$w0rd |
|
||||
Given user "participant1" creates room "room2" (v4)
|
||||
| roomType | 2 |
|
||||
| roomName | room2 |
|
||||
| password | P4$$w0rd |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | hasPassword |
|
||||
| room1 | 3 | 1 | 1 |
|
||||
| room2 | 2 | 1 | |
|
||||
|
||||
Scenario: Read only during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| readOnly | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | readOnly |
|
||||
| room | 3 | 1 | 1 |
|
||||
|
||||
Scenario: Listable during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| listable | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | listable |
|
||||
| room | 3 | 1 | 1 |
|
||||
|
||||
Scenario: Set message expiration during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| messageExpiration | 3600 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | messageExpiration |
|
||||
| room | 3 | 1 | 3600 |
|
||||
|
||||
Scenario: Set lobby during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| lobbyState | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | lobbyState | lobbyTimer |
|
||||
| room | 3 | 1 | 1 | 0 |
|
||||
|
||||
Scenario: Set lobby with timer during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| lobbyState | 1 |
|
||||
| lobbyTimer | OFFSET(3600) |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | lobbyState | lobbyTimer |
|
||||
| room | 3 | 1 | 1 | GREATER_THAN_ZERO |
|
||||
|
||||
Scenario: Enable SIP during creation
|
||||
Given the following "spreed" app config is set
|
||||
| sip_bridge_dialin_info | +49-1234-567890 |
|
||||
| sip_bridge_shared_secret | 1234567890abcdef |
|
||||
| sip_bridge_groups | ["group1"] |
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| sipEnabled | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | sipEnabled |
|
||||
| room | 3 | 1 | 1 |
|
||||
|
||||
Scenario: Set permissions during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| permissions | AV |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | defaultPermissions |
|
||||
| room | 3 | 1 | CAV |
|
||||
|
||||
Scenario: Set recording consent during creation
|
||||
And the following "spreed" app config is set
|
||||
| recording_consent | 2 |
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| recordingConsent | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | recordingConsent |
|
||||
| room | 3 | 1 | 1 |
|
||||
|
||||
Scenario: Set mention permissions during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| mentionPermissions | 1 |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | mentionPermissions |
|
||||
| room | 3 | 1 | 1 |
|
||||
|
||||
Scenario: Set description during creation
|
||||
Given user "participant1" creates room "room" (v4)
|
||||
| roomType | 3 |
|
||||
| roomName | room |
|
||||
| description | Lorem ipsum |
|
||||
Then user "participant1" is participant of the following rooms (v4)
|
||||
| id | type | participantType | description |
|
||||
| room | 3 | 1 | Lorem ipsum |
|
||||
|
|
@ -8,9 +8,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Talk\Tests\php\Service;
|
||||
|
||||
use OC\EmojiHelper;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\AvatarService;
|
||||
use OCA\Talk\Service\EmojiService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\IAvatarManager;
|
||||
|
|
@ -31,7 +31,7 @@ class AvatarServiceTest extends TestCase {
|
|||
protected ISecureRandom&MockObject $random;
|
||||
protected RoomService&MockObject $roomService;
|
||||
protected IAvatarManager&MockObject $avatarManager;
|
||||
protected EmojiHelper $emojiHelper;
|
||||
protected EmojiService $emojiService;
|
||||
protected ?AvatarService $service = null;
|
||||
|
||||
public function setUp(): void {
|
||||
|
|
@ -43,7 +43,7 @@ class AvatarServiceTest extends TestCase {
|
|||
$this->random = $this->createMock(ISecureRandom::class);
|
||||
$this->roomService = $this->createMock(RoomService::class);
|
||||
$this->avatarManager = $this->createMock(IAvatarManager::class);
|
||||
$this->emojiHelper = Server::get(EmojiHelper::class);
|
||||
$this->emojiService = Server::get(EmojiService::class);
|
||||
$this->service = new AvatarService(
|
||||
$this->appData,
|
||||
$this->l,
|
||||
|
|
@ -51,7 +51,7 @@ class AvatarServiceTest extends TestCase {
|
|||
$this->random,
|
||||
$this->roomService,
|
||||
$this->avatarManager,
|
||||
$this->emojiHelper,
|
||||
$this->emojiService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -78,19 +78,4 @@ class AvatarServiceTest extends TestCase {
|
|||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
}
|
||||
|
||||
public static function dataGetFirstCombinedEmoji(): array {
|
||||
return [
|
||||
['👋 Hello', '👋'],
|
||||
['Only leading emojis 🚀', ''],
|
||||
['👩🏽💻👩🏻💻👨🏿💻 Only one, but with all attributes', '👩🏽💻'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataGetFirstCombinedEmoji
|
||||
*/
|
||||
public function testGetFirstCombinedEmoji(string $roomName, string $avatarEmoji): void {
|
||||
$this->assertSame($avatarEmoji, self::invokePrivate($this->service, 'getFirstCombinedEmoji', [$roomName]));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
tests/php/Service/EmojiServiceTest.php
Normal file
44
tests/php/Service/EmojiServiceTest.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Tests\php\Service;
|
||||
|
||||
use OCA\Talk\Service\EmojiService;
|
||||
use OCP\IEmojiHelper;
|
||||
use OCP\Server;
|
||||
use Test\TestCase;
|
||||
|
||||
/**
|
||||
* @group DB
|
||||
*/
|
||||
class EmojiServiceTest extends TestCase {
|
||||
protected ?EmojiService $service = null;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->service = new EmojiService(
|
||||
Server::get(IEmojiHelper::class),
|
||||
);
|
||||
}
|
||||
|
||||
public static function dataGetFirstCombinedEmoji(): array {
|
||||
return [
|
||||
['👋 Hello', '👋'],
|
||||
['Only leading emojis 🚀', ''],
|
||||
['👩🏽💻👩🏻💻👨🏿💻 Only one, but with all attributes', '👩🏽💻'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataGetFirstCombinedEmoji
|
||||
*/
|
||||
public function testGetFirstCombinedEmoji(string $roomName, string $avatarEmoji): void {
|
||||
$this->assertSame($avatarEmoji, self::invokePrivate($this->service, 'getFirstCombinedEmoji', [$roomName]));
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ use OCA\Talk\Model\Attendee;
|
|||
use OCA\Talk\Model\BreakoutRoom;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\EmojiService;
|
||||
use OCA\Talk\Service\ParticipantService;
|
||||
use OCA\Talk\Service\RecordingService;
|
||||
use OCA\Talk\Service\RoomService;
|
||||
|
|
@ -29,6 +30,7 @@ use OCP\IDBConnection;
|
|||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
use OCP\Security\IHasher;
|
||||
use OCP\Server;
|
||||
use OCP\Share\IManager as IShareManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -48,6 +50,7 @@ class RoomServiceTest extends TestCase {
|
|||
protected IJobList&MockObject $jobList;
|
||||
protected LoggerInterface&MockObject $logger;
|
||||
protected IL10N&MockObject $l10n;
|
||||
protected EmojiService $emojiService;
|
||||
protected ?RoomService $service = null;
|
||||
|
||||
public function setUp(): void {
|
||||
|
|
@ -63,6 +66,7 @@ class RoomServiceTest extends TestCase {
|
|||
$this->jobList = $this->createMock(IJobList::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->l10n = $this->createMock(IL10N::class);
|
||||
$this->emojiService = Server::get(EmojiService::class);
|
||||
$this->service = new RoomService(
|
||||
$this->manager,
|
||||
$this->participantService,
|
||||
|
|
@ -73,6 +77,7 @@ class RoomServiceTest extends TestCase {
|
|||
$this->hasher,
|
||||
$this->dispatcher,
|
||||
$this->jobList,
|
||||
$this->emojiService,
|
||||
$this->logger,
|
||||
$this->l10n,
|
||||
);
|
||||
|
|
@ -225,8 +230,8 @@ class RoomServiceTest extends TestCase {
|
|||
|
||||
public static function dataCreateConversationInvalidObjects(): array {
|
||||
return [
|
||||
[str_repeat('a', 65), 'a', 'object_type'],
|
||||
['a', str_repeat('a', 65), 'object_id'],
|
||||
[str_repeat('a', 65), 'a', 'object-type'],
|
||||
['a', str_repeat('a', 65), 'object-id'],
|
||||
['a', '', 'object'],
|
||||
['', 'b', 'object'],
|
||||
];
|
||||
|
|
@ -247,9 +252,10 @@ class RoomServiceTest extends TestCase {
|
|||
public static function dataCreateConversation(): array {
|
||||
return [
|
||||
[Room::TYPE_GROUP, 'Group conversation', 'admin', '', '', ''],
|
||||
[Room::TYPE_PUBLIC, 'Public conversation', '', 'files', '123456', ''],
|
||||
[Room::TYPE_PUBLIC, 'Public conversation', '', 'files', '123456', 'AGoodPassword123?'],
|
||||
[Room::TYPE_CHANGELOG, 'Talk updates ✅', 'test1', 'changelog', 'conversation', ''],
|
||||
[Room::TYPE_PUBLIC, 'Public conversation', '', 'file', '123456', ''],
|
||||
[Room::TYPE_PUBLIC, 'Public conversation', '', 'file', '123456', 'AGoodPassword123?'],
|
||||
[Room::TYPE_CHANGELOG, 'Talk updates ✅', 'test1', '', '', ''],
|
||||
[Room::TYPE_GROUP, 'Let\'s get started!', 'test1', 'sample', 'test1', ''],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +347,7 @@ class RoomServiceTest extends TestCase {
|
|||
$this->hasher,
|
||||
$dispatcher,
|
||||
$this->jobList,
|
||||
$this->emojiService,
|
||||
$this->logger,
|
||||
$this->l10n,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue