diff --git a/appinfo/routes.php b/appinfo/routes.php index 6fba9ee5a11..642e1a4d431 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -39,6 +39,7 @@ include(__DIR__ . '/routes/routesPublicShareAuthController.php'), include(__DIR__ . '/routes/routesReactionController.php'), include(__DIR__ . '/routes/routesRoomController.php'), + include(__DIR__ . '/routes/routesRoomAvatarController.php'), include(__DIR__ . '/routes/routesSettingsController.php'), include(__DIR__ . '/routes/routesSignalingController.php'), include(__DIR__ . '/routes/routesTempAvatarController.php'), diff --git a/appinfo/routes/routesRoomAvatarController.php b/appinfo/routes/routesRoomAvatarController.php new file mode 100644 index 00000000000..abc437dd4a8 --- /dev/null +++ b/appinfo/routes/routesRoomAvatarController.php @@ -0,0 +1,40 @@ + + * + * @author Vitor Mattos + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +$requirements = [ + 'apiVersion' => 'v1', + 'token' => '^[a-z0-9]{4,30}$', +]; + +return [ + 'ocs' => [ + /** @see \OCA\Talk\Controller\RoomAvatarController::getAvatar() */ + ['name' => 'RoomAvatar#getAvatar', 'url' => '/api/{apiVersion}/avatar/{roomToken}/{size}', 'verb' => 'GET', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\RoomAvatarController::setAvatar() */ + ['name' => 'RoomAvatar#setAvatar', 'url' => '/api/{apiVersion}/avatar/{roomToken}', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\RoomAvatarController::deleteAvatar() */ + ['name' => 'RoomAvatar#deleteAvatar', 'url' => '/api/{apiVersion}/avatar/{roomToken}', 'verb' => 'DELETE', 'requirements' => $requirements], + ], +]; diff --git a/docs/capabilities.md b/docs/capabilities.md index 9659e74283b..d3ddde1dd71 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -63,6 +63,7 @@ title: Capabilities * `phonebook-search` - Is present when the server has the endpoint to search for phone numbers to find matches in the accounts list * `raise-hand` - Participants can raise or lower hand, the state change is sent through signaling messages. * `room-description` - A description can be get and set for conversations. +* `room-avatar` - A custom picture can be got and set for conversations. * `config => chat => read-privacy` - See `chat-read-status` * `config => previews => max-gif-size` - Maximum size in bytes below which a GIF can be embedded directly in the page at render time. Bigger files will be rendered statically using the preview endpoint instead. Can be set with `occ config:app:set spreed max-gif-size --value=X` where X is the new value in bytes. Defaults to 3 MB. diff --git a/docs/conversation.md b/docs/conversation.md index 4b18e59d9bf..72d37bfe55a 100644 --- a/docs/conversation.md +++ b/docs/conversation.md @@ -44,6 +44,8 @@ | `name` | string | v1 | | Name of the conversation (can also be empty) | | `displayName` | string | v1 | | `name` if non empty, otherwise it falls back to a list of participants | | `description` | string | v3 | | Description of the conversation (can also be empty) (only available with `room-description` capability) | +| `avatarId` | string | v3 | | The type of the avatar ("custom", "user", "icon-public", "icon-contacts", "icon-mail", "icon-password", "icon-changelog", "icon-file") (only available with `room-avatar` capability) | +| `avatarVersion` | int | v3 | | The version of the avatar (only available with `room-avatar` capability) | | `participantType` | int | v1 | | Permissions level of the current user | | `attendeeId` | int | v3 | | Unique attendee id | | `attendeePin` | string | v3 | | Unique dial-in authentication code for this user, when the conversation has SIP enabled (see `sipEnabled` attribute) | diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 76aa2e1d7dd..cefe4a036ac 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -30,6 +30,7 @@ use OCA\Circles\Events\RemovingCircleMemberEvent; use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; use OCA\Talk\Activity\Listener as ActivityListener; +use OCA\Talk\Avatar\Listener as AvatarListener; use OCA\Talk\Capabilities; use OCA\Talk\Chat\Changelog\Listener as ChangelogListener; use OCA\Talk\Chat\ChatManager; @@ -181,6 +182,7 @@ public function boot(IBootContext $context): void { } ShareListener::register($dispatcher); StatusListener::register($dispatcher); + AvatarListener::register($dispatcher); $this->registerChatHooks($dispatcher); $context->injectFn(\Closure::fromCallable([$this, 'registerCloudFederationProviderManager'])); diff --git a/lib/Avatar/Listener.php b/lib/Avatar/Listener.php new file mode 100644 index 00000000000..c77de003fbe --- /dev/null +++ b/lib/Avatar/Listener.php @@ -0,0 +1,84 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Avatar; + +use OCA\Talk\Manager; +use OCA\Talk\Room; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use Symfony\Component\EventDispatcher\GenericEvent; + +class Listener { + + /** @var Manager */ + private $manager; + + /** + * @param Manager $manager + */ + public function __construct( + Manager $manager) { + $this->manager = $manager; + } + + public static function register(IEventDispatcher $dispatcher): void { + $listener = static function (GenericEvent $event): void { + if ($event->getArgument('feature') !== 'avatar') { + return; + } + + /** @var self $listener */ + $listener = \OC::$server->query(self::class); + $listener->updateRoomAvatarsFromChangedUserAvatar($event->getSubject()); + }; + $dispatcher->addListener(IUser::class . '::changeUser', $listener); + } + + /** + * Updates the associated room avatars from the changed user avatar + * + * The avatar versions of all the one-to-one conversations of that user are + * bumped. + * + * Note that the avatar seen by the user who has changed her avatar will not + * change, as she will get the avatar of the other user, but even if the + * avatar images are independent the avatar version is a shared value and + * needs to be bumped for both. + * + * @param IUser $user the user whose avatar changed + */ + public function updateRoomAvatarsFromChangedUserAvatar(IUser $user): void { + $rooms = $this->manager->getRoomsForUser($user->getUID()); + foreach ($rooms as $room) { + if ($room->getType() !== Room::ONE_TO_ONE_CALL) { + continue; + } + + $room->setAvatar($room->getAvatarId(), $room->getAvatarVersion() + 1); + } + } +} diff --git a/lib/Avatar/RoomAvatar.php b/lib/Avatar/RoomAvatar.php new file mode 100644 index 00000000000..5dc1f7ab1ce --- /dev/null +++ b/lib/Avatar/RoomAvatar.php @@ -0,0 +1,383 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Avatar; + +use OCA\Talk\Room; +use OCP\Color; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IAvatar; +use OCP\IImage; +use OCP\IL10N; +use OCP\Image; +use Psr\Log\LoggerInterface; + +class RoomAvatar implements IAvatar { + + /** @var ISimpleFolder */ + private $folder; + + /** @var Room */ + private $room; + + /** @var IL10N */ + private $l; + + /** @var LoggerInterface */ + private $logger; + + /** @var Util */ + private $util; + + public function __construct( + ISimpleFolder $folder, + Room $room, + IL10N $l, + LoggerInterface $logger, + Util $util) { + $this->folder = $folder; + $this->room = $room; + $this->l = $l; + $this->logger = $logger; + $this->util = $util; + } + + public function getRoom(): Room { + return $this->room; + } + + /** + * Returns the default room avatar type ("user", "icon-public", + * "icon-contacts"...) for the given room data + * + * @param int $roomType the type of the room + * @param string $objectType the object type of the room + * @return string the room avatar type + */ + public static function getDefaultRoomAvatarType(int $roomType, string $objectType): string { + if ($roomType === Room::ONE_TO_ONE_CALL) { + return 'user'; + } + + if ($objectType === 'emails') { + return 'icon-mail'; + } + + if ($objectType === 'file') { + return 'icon-file'; + } + + if ($objectType === 'share:password') { + return 'icon-password'; + } + + if ($roomType === Room::CHANGELOG_CONVERSATION) { + return 'icon-changelog'; + } + + if ($roomType === Room::GROUP_CALL) { + return 'icon-contacts'; + } + + return 'icon-public'; + } + + /** + * Returns the room avatar type ("custom", "user", "icon-public", + * "icon-contacts"...) of this RoomAvatar + * + * @return string the room avatar type + */ + public function getRoomAvatarType(): string { + if ($this->isCustomAvatar()) { + return 'custom'; + } + + return self::getDefaultRoomAvatarType($this->room->getType(), $this->room->getObjectType()); + } + + /** + * Gets the room avatar + * + * @param int $size size in px of the avatar, avatars are square, defaults + * to 64, -1 can be used to not scale the image + * @return bool|\OCP\IImage containing the avatar or false if there is no + * image + */ + public function get($size = 64) { + $size = (int) $size; + + try { + $file = $this->getFile($size); + } catch (NotFoundException $e) { + return false; + } + + $avatar = new Image(); + $avatar->loadFromData($file->getContent()); + return $avatar; + } + + /** + * Checks if an avatar exists for the room + * + * @return bool + */ + public function exists(): bool { + return $this->folder->fileExists('avatar.jpg') || $this->folder->fileExists('avatar.png'); + } + + /** + * Checks if the avatar of a room is a custom uploaded one + * + * @return bool + */ + public function isCustomAvatar(): bool { + return $this->exists(); + } + + /** + * Sets the room avatar + * + * @param \OCP\IImage|resource|string $data An image object, imagedata or + * path to set a new avatar + * @throws \Exception if the provided file is not a jpg or png image + * @throws \Exception if the provided image is not valid + * @return void + */ + public function set($data): void { + $image = $this->getAvatarImage($data); + $data = $image->data(); + + $this->validateAvatar($image); + + $this->removeFiles(); + $type = $this->getAvatarImageType($image); + $file = $this->folder->newFile('avatar.' . $type); + $file->putContent($data); + + $this->room->setAvatar($this->getRoomAvatarType(), $this->room->getAvatarVersion() + 1); + } + + /** + * Returns an image from several sources + * + * @param IImage|resource|string $data An image object, imagedata or path to + * the avatar + * @return IImage + */ + private function getAvatarImage($data): IImage { + if ($data instanceof IImage) { + return $data; + } + + $image = new Image(); + if (is_resource($data) && get_resource_type($data) === 'gd') { + $image->setResource($data); + } elseif (is_resource($data)) { + $image->loadFromFileHandle($data); + } else { + try { + // detect if it is a path or maybe the images as string + $result = @realpath($data); + if ($result === false || $result === null) { + $image->loadFromData($data); + } else { + $image->loadFromFile($data); + } + } catch (\Error $e) { + $image->loadFromData($data); + } + } + + return $image; + } + + /** + * Returns the avatar image type + * + * @param IImage $avatar + * @return string + */ + private function getAvatarImageType(IImage $avatar): string { + $type = substr($avatar->mimeType(), -3); + if ($type === 'peg') { + $type = 'jpg'; + } + return $type; + } + + /** + * Validates an avatar image: + * - must be "png" or "jpg" + * - must be "valid" + * - must be in square format + * + * @param IImage $avatar The avatar to validate + * @throws \Exception if the provided file is not a jpg or png image + * @throws \Exception if the provided image is not valid + * @throws \Exception if the image is not square + */ + private function validateAvatar(IImage $avatar): void { + $type = $this->getAvatarImageType($avatar); + + if ($type !== 'jpg' && $type !== 'png') { + throw new \Exception($this->l->t('Unknown filetype')); + } + + if (!$avatar->valid()) { + throw new \Exception($this->l->t('Invalid image')); + } + + if (!($avatar->height() === $avatar->width())) { + throw new \Exception($this->l->t('Avatar image is not square')); + } + } + + /** + * Remove the room avatar + * + * @param bool $silent Whether removing the avatar should trigger a change + * @return void + */ + public function remove(bool $silent = false): void { + $this->removeFiles(); + + $this->room->setAvatar($this->getRoomAvatarType(), $this->room->getAvatarVersion() + 1); + } + + /** + * Remove the files for the room avatar + * + * @return void + */ + private function removeFiles(): void { + $files = $this->folder->getDirectoryListing(); + + // Deletes the original image as well as the resized ones. + foreach ($files as $file) { + $file->delete(); + } + } + + /** + * Get the file of the avatar + * + * @param int $size -1 can be used to not scale the image + * + * @throws NotFoundException + */ + public function getFile(int $size): ISimpleFile { + $size = (int) $size; + + if ($this->room->getType() === Room::ONE_TO_ONE_CALL) { + $userAvatar = $this->util->getUserAvatarForOtherParticipant($this->room); + + return $userAvatar->getFile($size); + } + + $extension = $this->getExtension(); + + if ($size === -1) { + $path = 'avatar.' . $extension; + } else { + $path = 'avatar.' . $size . '.' . $extension; + } + + try { + $file = $this->folder->getFile($path); + } catch (NotFoundException $e) { + if ($size <= 0) { + throw new NotFoundException(); + } + + $file = $this->generateResizedAvatarFile($extension, $path, $size); + } + + return $file; + } + + /** + * Gets the extension of the avatar file + * + * @return string the extension + * @throws NotFoundException if there is no avatar + */ + private function getExtension(): string { + if ($this->folder->fileExists('avatar.jpg')) { + return 'jpg'; + } + if ($this->folder->fileExists('avatar.png')) { + return 'png'; + } + throw new NotFoundException; + } + + /** + * Generates a resized avatar file with the given size + * + * @param string $extension the extension of the original avatar file + * @param string $path the path to the resized avatar file + * @param int $size the size of the avatar + * @return ISimpleFile the resized avatar file + * @throws NotFoundException if it was not possible to generate the resized + * avatar file + */ + private function generateResizedAvatarFile(string $extension, string $path, int $size): ISimpleFile { + $avatar = new Image(); + $file = $this->folder->getFile('avatar.' . $extension); + $avatar->loadFromData($file->getContent()); + $avatar->resize($size); + $data = $avatar->data(); + + try { + $file = $this->folder->newFile($path); + $file->putContent($data); + } catch (NotPermittedException $e) { + $this->logger->error('Failed to save avatar for room ' . $this->room->getToken() . ' with size ' . $size); + throw new NotFoundException(); + } + + return $file; + } + + /** + * Ignored. + */ + public function avatarBackgroundColor(string $hash): Color { + // Unused, unneeded, so just return + return new Color(0, 0, 0); + } + + /** + * Ignored. + */ + public function userChanged(string $feature, $oldValue, $newValue): void { + } +} diff --git a/lib/Avatar/RoomAvatarProvider.php b/lib/Avatar/RoomAvatarProvider.php new file mode 100644 index 00000000000..b44d62b0af0 --- /dev/null +++ b/lib/Avatar/RoomAvatarProvider.php @@ -0,0 +1,180 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Avatar; + +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Manager; +use OCA\Talk\Room; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\IAvatar; +use OCP\IL10N; +use Psr\Log\LoggerInterface; + +class RoomAvatarProvider { + + /** @var IAppData */ + private $appData; + + /** @var Manager */ + private $manager; + + /** @var IL10N */ + private $l; + + /** @var LoggerInterface */ + private $logger; + + /** @var Util */ + private $util; + + public function __construct( + IAppData $appData, + Manager $manager, + IL10N $l, + LoggerInterface $logger, + Util $util) { + $this->appData = $appData; + $this->manager = $manager; + $this->l = $l; + $this->logger = $logger; + $this->util = $util; + } + + /** + * Returns a RoomAvatar instance for the given room token + * + * @param string $id the identifier of the avatar + * @returns IAvatar the RoomAvatar + * @throws RoomNotFoundException if there is no room with the given token + */ + public function getAvatar(string $id): IAvatar { + $room = $this->manager->getRoomByToken($id); + + try { + $folder = $this->appData->getFolder('avatar/' . $id); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder('avatar/' . $id); + } + + return new RoomAvatar($folder, $room, $this->l, $this->logger, $this->util); + } + + /** + * Returns whether the current user can access the given avatar or not + * + * @param IAvatar $avatar the avatar to check + * @return bool true if the room is public, the current user is a + * participant of the room or can list it, false otherwise + * @throws \InvalidArgumentException if the given avatar is not a RoomAvatar + */ + public function canBeAccessedByCurrentUser(IAvatar $avatar): bool { + if (!($avatar instanceof RoomAvatar)) { + throw new \InvalidArgumentException(); + } + + $room = $avatar->getRoom(); + + if ($room->getType() === Room::PUBLIC_CALL) { + return true; + } + + try { + $this->util->getCurrentParticipant($room); + } catch (ParticipantNotFoundException $e) { + return $this->util->isRoomListableByUser($room); + } + + return true; + } + + /** + * Returns whether the current user can modify the given avatar or not + * + * @param IAvatar $avatar the avatar to check + * @return bool true if the current user is a moderator of the room and the + * room is not a one-to-one, password request or file room, false + * otherwise + * @throws \InvalidArgumentException if the given avatar is not a RoomAvatar + */ + public function canBeModifiedByCurrentUser(IAvatar $avatar): bool { + if (!($avatar instanceof RoomAvatar)) { + throw new \InvalidArgumentException(); + } + + $room = $avatar->getRoom(); + + if ($room->getType() === Room::ONE_TO_ONE_CALL) { + return false; + } + + if ($room->getObjectType() === 'share:password') { + return false; + } + + if ($room->getObjectType() === 'file') { + return false; + } + + try { + $currentParticipant = $this->util->getCurrentParticipant($room); + } catch (ParticipantNotFoundException $e) { + return false; + } + + return $currentParticipant->hasModeratorPermissions(); + } + + /** + * Returns the latest value of the avatar version + * + * @param IAvatar $avatar + * @return int + * @throws \InvalidArgumentException if the given avatar is not a RoomAvatar + */ + public function getVersion(IAvatar $avatar): int { + if (!($avatar instanceof RoomAvatar)) { + throw new \InvalidArgumentException(); + } + + $room = $avatar->getRoom(); + + return $room->getAvatarVersion(); + } + + /** + * Returns the cache duration for room avatars in seconds + * + * @param IAvatar $avatar ignored, same duration for all room avatars + * @return int|null the cache duration + */ + public function getCacheTimeToLive(IAvatar $avatar): ?int { + // Cache for 1 day. + return 60 * 60 * 24; + } +} diff --git a/lib/Avatar/Util.php b/lib/Avatar/Util.php new file mode 100644 index 00000000000..b9c6c13114c --- /dev/null +++ b/lib/Avatar/Util.php @@ -0,0 +1,126 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Avatar; + +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Manager; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCA\Talk\TalkSession; +use OCP\IAvatar; +use OCP\IAvatarManager; + +class Util { + + /** @var string|null */ + protected $userId; + + /** @var TalkSession */ + protected $session; + + /** @var IAvatarManager */ + private $avatarManager; + + /** @var Manager */ + private $manager; + + /** @var ParticipantService */ + private $participantService; + + /** + * @param string|null $userId + * @param TalkSession $session + * @param IAvatarManager $avatarManager + * @param Manager $manager + * @param ParticipantService $participantService + */ + public function __construct( + ?string $userId, + TalkSession $session, + IAvatarManager $avatarManager, + Manager $manager, + ParticipantService $participantService) { + $this->userId = $userId; + $this->session = $session; + $this->avatarManager = $avatarManager; + $this->manager = $manager; + $this->participantService = $participantService; + } + + /** + * @param Room $room + * @return Participant + * @throws ParticipantNotFoundException + */ + public function getCurrentParticipant(Room $room): Participant { + $participant = null; + try { + $participant = $room->getParticipant($this->userId); + } catch (ParticipantNotFoundException $e) { + $participant = $room->getParticipantBySession($this->session->getSessionForRoom($room->getToken())); + } + + return $participant; + } + + /** + * @param Room $room + * @return bool + */ + public function isRoomListableByUser(Room $room): bool { + return $this->manager->isRoomListableByUser($room, $this->userId); + } + + /** + * @param Room $room + * @return IAvatar + * @throws \InvalidArgumentException if the given room is not a one-to-one + * room, the current participant is not a member of the room or + * there is no other participant in the room + */ + public function getUserAvatarForOtherParticipant(Room $room): IAvatar { + if ($room->getType() !== Room::ONE_TO_ONE_CALL) { + throw new \InvalidArgumentException('Not a one-to-one room'); + } + + $userIds = $this->participantService->getParticipantUserIds($room); + if (array_search($this->userId, $userIds) === false) { + throw new \InvalidArgumentException('Current participant is not a member of the room'); + } + if (count($userIds) < 2) { + throw new \InvalidArgumentException('No other participant in the room'); + } + + $otherParticipantUserId = $userIds[0]; + if ($otherParticipantUserId === $this->userId) { + $otherParticipantUserId = $userIds[1]; + } + + return $this->avatarManager->getAvatar($otherParticipantUserId); + } +} diff --git a/lib/Capabilities.php b/lib/Capabilities.php index c388347be55..4f621bce080 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -112,6 +112,7 @@ public function getCapabilities(): array { 'silent-call', 'send-call-notification', 'talk-polls', + 'room-avatar', ], 'config' => [ 'attachments' => [ diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index 225a639ab65..ebcb50c2bcf 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -154,6 +154,16 @@ public function parseMessage(Message $chatMessage): void { } elseif ($cliIsActor) { $parsedMessage = $this->l->t('An administrator removed the description'); } + } elseif ($message === 'avatar_set') { + $parsedMessage = $this->l->t('{actor} set the conversation picture'); + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You set the conversation picture'); + } + } elseif ($message === 'avatar_removed') { + $parsedMessage = $this->l->t('{actor} removed the conversation picture'); + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You removed the conversation picture'); + } } elseif ($message === 'call_started') { $parsedMessage = $this->l->t('{actor} started a call'); if ($currentUserIsActor) { diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index ef012cb3fd4..41337a70a04 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -84,6 +84,7 @@ public static function register(IEventDispatcher $dispatcher): void { $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CREATE, self::class . '::sendSystemMessageAboutConversationCreated'); $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, self::class . '::sendSystemMessageAboutConversationRenamed'); $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, self::class . '::sendSystemMessageAboutRoomDescriptionChanges'); + $dispatcher->addListener(Room::EVENT_AFTER_AVATAR_SET, self::class . '::sendSystemMessageAfterAvatarSet'); $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, self::class . '::sendSystemMessageAboutRoomPassword'); $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, self::class . '::sendSystemGuestPermissionsMessage'); $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, self::class . '::sendSystemReadOnlyMessage'); @@ -168,6 +169,18 @@ public static function sendSystemMessageAboutRoomDescriptionChanges(ModifyRoomEv } } + public static function sendSystemMessageAfterAvatarSet(ModifyRoomEvent $event): void { + $room = $event->getRoom(); + /** @var self $listener */ + $listener = \OC::$server->get(self::class); + + if ($event->getNewValue() === 'custom') { + $listener->sendSystemMessage($room, 'avatar_set'); + } elseif ($event->getNewValue() !== 'custom' && $event->getOldValue() === 'custom') { + $listener->sendSystemMessage($room, 'avatar_removed'); + } + } + public static function sendSystemMessageAboutRoomPassword(ModifyRoomEvent $event): void { $room = $event->getRoom(); $listener = Server::get(self::class); diff --git a/lib/Controller/RoomAvatarController.php b/lib/Controller/RoomAvatarController.php new file mode 100644 index 00000000000..fd701039ad7 --- /dev/null +++ b/lib/Controller/RoomAvatarController.php @@ -0,0 +1,233 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Controller; + +use OCA\Talk\Avatar\RoomAvatarProvider; +use OCP\AppFramework\OCSController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\Response; +use OCP\Files\NotFoundException; +use OCP\IL10N; +use OCP\Image; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class RoomAvatarController extends OCSController { + + /** @var IL10N */ + protected $l; + + /** @var LoggerInterface */ + protected $logger; + + /** @var RoomAvatarProvider */ + protected $roomAvatarProvider; + + public function __construct($appName, + IRequest $request, + IL10N $l10n, + LoggerInterface $logger, + RoomAvatarProvider $roomAvatarProvider) { + parent::__construct($appName, $request); + + $this->l = $l10n; + $this->logger = $logger; + $this->roomAvatarProvider = $roomAvatarProvider; + } + + /** + * @PublicPage + * + * @param string $roomToken + * @param int $size + * @return DataResponse|FileDisplayResponse + */ + public function getAvatar(string $roomToken, int $size): Response { + $size = $this->sanitizeSize($size); + + try { + $avatar = $this->roomAvatarProvider->getAvatar($roomToken); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$this->roomAvatarProvider->canBeAccessedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatarFile = $avatar->getFile($size); + $response = new FileDisplayResponse( + $avatarFile, + Http::STATUS_OK, + [ + 'Content-Type' => $avatarFile->getMimeType(), + 'X-NC-IsCustomAvatar' => $avatar->isCustomAvatar() ? '1' : '0', + ] + ); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + $cache = $this->roomAvatarProvider->getCacheTimeToLive($avatar); + if ($cache !== null) { + $response->cacheFor($cache); + } + + return $response; + } + + /** + * Returns the closest value to the predefined set of sizes + * + * @param int $size the size to sanitize + * @return int the sanitized size + */ + private function sanitizeSize(int $size): int { + $validSizes = [64, 128, 256, 512]; + + if ($size < $validSizes[0]) { + return $validSizes[0]; + } + + if ($size > $validSizes[count($validSizes) - 1]) { + return $validSizes[count($validSizes) - 1]; + } + + for ($i = 0; $i < count($validSizes) - 1; $i++) { + if ($size >= $validSizes[$i] && $size <= $validSizes[$i + 1]) { + $middlePoint = ($validSizes[$i] + $validSizes[$i + 1]) / 2; + if ($size < $middlePoint) { + return $validSizes[$i]; + } + return $validSizes[$i + 1]; + } + } + + return $size; + } + + /** + * @PublicPage + * + * @param string $roomToken + * @return DataResponse + */ + public function setAvatar(string $roomToken): DataResponse { + $files = $this->request->getUploadedFile('files'); + + if (is_null($files)) { + return new DataResponse( + ['data' => ['message' => $this->l->t('No file provided')]], + Http::STATUS_BAD_REQUEST + ); + } + + if ( + $files['error'][0] !== 0 || + !is_uploaded_file($files['tmp_name'][0]) || + \OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0]) + ) { + return new DataResponse( + ['data' => ['message' => $this->l->t('Invalid file provided')]], + Http::STATUS_BAD_REQUEST + ); + } + + if ($files['size'][0] > 20 * 1024 * 1024) { + return new DataResponse( + ['data' => ['message' => $this->l->t('File is too big')]], + Http::STATUS_BAD_REQUEST + ); + } + + $content = file_get_contents($files['tmp_name'][0]); + unlink($files['tmp_name'][0]); + + $image = new Image(); + $image->loadFromData($content); + + try { + $avatar = $this->roomAvatarProvider->getAvatar($roomToken); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$this->roomAvatarProvider->canBeModifiedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatar->set($image); + return new DataResponse( + ['status' => 'success'] + ); + } catch (\OC\NotSquareException $e) { + return new DataResponse( + ['data' => ['message' => $this->l->t('Crop is not square')]], + Http::STATUS_BAD_REQUEST + ); + } catch (\Exception $e) { + $this->logger->error('Error when setting avatar', ['app' => 'core', 'exception' => $e]); + return new DataResponse( + ['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], + Http::STATUS_BAD_REQUEST + ); + } + } + + /** + * @PublicPage + * + * @param string $roomToken + * @return DataResponse + */ + public function deleteAvatar(string $roomToken): DataResponse { + try { + $avatar = $this->roomAvatarProvider->getAvatar($roomToken); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$this->roomAvatarProvider->canBeModifiedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatar->remove(); + return new DataResponse(); + } catch (\Exception $e) { + $this->logger->error('Error when deleting avatar', ['app' => 'core', 'exception' => $e]); + return new DataResponse( + ['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], + Http::STATUS_BAD_REQUEST + ); + } + } +} diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index a9756ee6ede..3956ccce072 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -408,6 +408,8 @@ protected function formatRoomV4(Room $room, ?Participant $currentParticipant, ?a 'canEnableSIP' => false, 'attendeePin' => '', 'description' => '', + 'avatarId' => '', + 'avatarVersion' => 0, 'lastCommonReadMessage' => 0, 'listable' => Room::LISTABLE_NONE, 'callFlag' => Participant::FLAG_DISCONNECTED, @@ -477,6 +479,8 @@ protected function formatRoomV4(Room $room, ?Participant $currentParticipant, ?a 'callPermissions' => $room->getCallPermissions(), 'defaultPermissions' => $room->getDefaultPermissions(), 'description' => $room->getDescription(), + 'avatarId' => $room->getAvatarId(), + 'avatarVersion' => $room->getAvatarVersion(), 'listable' => $room->getListable(), 'messageExpiration' => $room->getMessageExpiration(), ]); diff --git a/lib/Events/ModifyAvatarEvent.php b/lib/Events/ModifyAvatarEvent.php new file mode 100644 index 00000000000..a16d65438b9 --- /dev/null +++ b/lib/Events/ModifyAvatarEvent.php @@ -0,0 +1,51 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Events; + +use OCA\Talk\Room; + +class ModifyAvatarEvent extends ModifyRoomEvent { + + /** @var int */ + protected $avatarVersion; + + public function __construct(Room $room, + string $parameter, + string $newValue, + string $oldValue, + int $avatarVersion) { + parent::__construct($room, $parameter, $newValue, $oldValue); + $this->avatarVersion = $avatarVersion; + } + + /** + * @return int + */ + public function avatarVersion(): int { + return $this->avatarVersion; + } +} diff --git a/lib/Manager.php b/lib/Manager.php index 1e2798ee04d..8310e11e93f 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -23,6 +23,7 @@ namespace OCA\Talk; +use OCA\Talk\Avatar\RoomAvatar; use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; @@ -172,6 +173,8 @@ public function createRoomObject(array $row): Room { (string) $row['token'], (string) $row['name'], (string) $row['description'], + (string) $row['avatar_id'], + (int) $row['avatar_version'], (string) $row['password'], (string) $row['remote_server'], (string) $row['remote_token'], @@ -910,6 +913,8 @@ public function getChangelogRoom(string $userId): Room { public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = ''): Room { $token = $this->getNewToken(); + $defaultRoomAvatarType = RoomAvatar::getDefaultRoomAvatarType($type, $objectType); + $insert = $this->db->getQueryBuilder(); $insert->insert('talk_rooms') ->values( @@ -917,6 +922,7 @@ public function createRoom(int $type, string $name = '', string $objectType = '' 'name' => $insert->createNamedParameter($name), 'type' => $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT), 'token' => $insert->createNamedParameter($token), + 'avatar_id' => $insert->createNamedParameter($defaultRoomAvatarType), ] ); diff --git a/lib/Migration/Version11000Date20201229115215.php b/lib/Migration/Version11000Date20201229115215.php new file mode 100644 index 00000000000..e0587624f09 --- /dev/null +++ b/lib/Migration/Version11000Date20201229115215.php @@ -0,0 +1,101 @@ + + * + * @author Daniel Calviño Sánchez + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use Doctrine\DBAL\Types\Types; +use OCA\Talk\Avatar\RoomAvatar; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version11000Date20201229115215 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $changedSchema = false; + + $table = $schema->getTable('talk_rooms'); + if (!$table->hasColumn('avatar_id')) { + $table->addColumn('avatar_id', Types::STRING, [ + 'notnull' => false, + ]); + + $changedSchema = true; + } + if (!$table->hasColumn('avatar_version')) { + $table->addColumn('avatar_version', Types::INTEGER, [ + 'notnull' => true, + 'default' => 1, + ]); + + $changedSchema = true; + } + + return $changedSchema ? $schema : null; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $update = $this->connection->getQueryBuilder(); + $update->update('talk_rooms') + ->set('avatar_id', $update->createParameter('avatar_id')) + ->where($update->expr()->eq('id', $update->createParameter('id'))); + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('talk_rooms'); + + $result = $query->execute(); + while ($row = $result->fetch()) { + $defaultRoomAvatarType = RoomAvatar::getDefaultRoomAvatarType((int) $row['type'], (string) $row['object_type']); + $update->setParameter('avatar_id', $defaultRoomAvatarType) + ->setParameter('id', (int) $row['id']); + $update->execute(); + } + $result->closeCursor(); + } +} diff --git a/lib/Model/SelectHelper.php b/lib/Model/SelectHelper.php index 9ad2b5f0951..23299705bdd 100644 --- a/lib/Model/SelectHelper.php +++ b/lib/Model/SelectHelper.php @@ -39,6 +39,8 @@ public function selectRoomsTable(IQueryBuilder $query, string $alias = 'r'): voi ->addSelect($alias . 'token') ->addSelect($alias . 'name') ->addSelect($alias . 'description') + ->addSelect($alias . 'avatar_id') + ->addSelect($alias . 'avatar_version') ->addSelect($alias . 'password') ->addSelect($alias . 'active_guests') ->addSelect($alias . 'active_since') diff --git a/lib/Room.php b/lib/Room.php index 167fd1fc253..129221eb439 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -27,6 +27,7 @@ namespace OCA\Talk; +use OCA\Talk\Events\ModifyAvatarEvent; use OCA\Talk\Events\ModifyRoomEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Events\SignalingRoomPropertiesEvent; @@ -105,6 +106,8 @@ class Room { public const EVENT_AFTER_NAME_SET = self::class . '::postSetName'; public const EVENT_BEFORE_DESCRIPTION_SET = self::class . '::preSetDescription'; public const EVENT_AFTER_DESCRIPTION_SET = self::class . '::postSetDescription'; + public const EVENT_BEFORE_AVATAR_SET = self::class . '::preSetAvatar'; + public const EVENT_AFTER_AVATAR_SET = self::class . '::postSetAvatar'; public const EVENT_BEFORE_PASSWORD_SET = self::class . '::preSetPassword'; public const EVENT_AFTER_PASSWORD_SET = self::class . '::postSetPassword'; public const EVENT_BEFORE_TYPE_SET = self::class . '::preSetType'; @@ -170,6 +173,8 @@ class Room { private string $token; private string $name; private string $description; + private string $avatarId; + private int $avatarVersion; private string $password; private string $remoteServer; private string $remoteToken; @@ -203,6 +208,8 @@ public function __construct(Manager $manager, string $token, string $name, string $description, + string $avatarId, + int $avatarVersion, string $password, string $remoteServer, string $remoteToken, @@ -233,6 +240,8 @@ public function __construct(Manager $manager, $this->token = $token; $this->name = $name; $this->description = $description; + $this->avatarId = $avatarId; + $this->avatarVersion = $avatarVersion; $this->password = $password; $this->remoteServer = $remoteServer; $this->remoteToken = $remoteToken; @@ -376,6 +385,14 @@ public function setDescription(string $description): void { $this->description = $description; } + public function getAvatarId(): string { + return $this->avatarId; + } + + public function getAvatarVersion(): int { + return $this->avatarVersion; + } + /** * @deprecated Use ParticipantService::getGuestCount() instead * @return int @@ -477,6 +494,8 @@ public function getPropertiesForSignaling(string $userId, bool $roomModified = t 'listable' => $this->getListable(), 'active-since' => $this->getActiveSince(), 'sip-enabled' => $this->getSIPEnabled(), + 'avatarId' => $this->getAvatarId(), + 'avatarVersion' => $this->getAvatarVersion(), ]; if ($roomModified) { @@ -709,6 +728,41 @@ public function setName(string $newName, ?string $oldName = null): bool { return true; } + /** + * Sets the avatar id and version. + * + * @param string $avatarId + * @param int $avatarVersion + * @return bool True when the change was valid, false otherwise + */ + public function setAvatar(string $avatarId, int $avatarVersion): bool { + $oldAvatarId = $this->getAvatarId(); + $oldAvatarVersion = $this->getAvatarVersion(); + if ($avatarId === $oldAvatarId && $avatarVersion === $oldAvatarVersion) { + return false; + } + + if ($avatarVersion <= $oldAvatarVersion) { + return false; + } + + $event = new ModifyAvatarEvent($this, 'avatarId', $avatarId, $oldAvatarId, $avatarVersion); + $this->dispatcher->dispatch(self::EVENT_BEFORE_AVATAR_SET, $event); + + $query = $this->db->getQueryBuilder(); + $query->update('talk_rooms') + ->set('avatar_id', $query->createNamedParameter($avatarId)) + ->set('avatar_version', $query->createNamedParameter($avatarVersion, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); + $query->execute(); + $this->avatarId = $avatarId; + $this->avatarVersion = $avatarVersion; + + $this->dispatcher->dispatch(self::EVENT_AFTER_AVATAR_SET, $event); + + return true; + } + /** * @param \DateTime $now * @return bool diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 566641d9ed9..5f014a9fedb 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -74,6 +74,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, [self::class, 'notifyAfterUsersAdd']); $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, [self::class, 'notifyAfterRoomSettingsChanged']); $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, [self::class, 'notifyAfterRoomSettingsChanged']); + $dispatcher->addListener(Room::EVENT_AFTER_AVATAR_SET, [self::class, 'notifyAfterRoomSettingsChanged']); $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, [self::class, 'notifyAfterRoomSettingsChanged']); $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, [self::class, 'notifyAfterRoomSettingsChanged']); $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, [self::class, 'notifyAfterRoomSettingsChanged']); diff --git a/tests/integration/data/blue-square-256.jpg b/tests/integration/data/blue-square-256.jpg new file mode 100644 index 00000000000..13eb46a10a8 Binary files /dev/null and b/tests/integration/data/blue-square-256.jpg differ diff --git a/tests/integration/data/green-rectangle-256-128.png b/tests/integration/data/green-rectangle-256-128.png new file mode 100644 index 00000000000..ff809095094 Binary files /dev/null and b/tests/integration/data/green-rectangle-256-128.png differ diff --git a/tests/integration/data/green-square-256.png b/tests/integration/data/green-square-256.png new file mode 100644 index 00000000000..9f14b707ca3 Binary files /dev/null and b/tests/integration/data/green-square-256.png differ diff --git a/tests/integration/data/textfile.txt b/tests/integration/data/textfile.txt new file mode 100644 index 00000000000..efffdeff159 --- /dev/null +++ b/tests/integration/data/textfile.txt @@ -0,0 +1,3 @@ +This is a testfile. + +Cheers. \ No newline at end of file diff --git a/tests/integration/features/bootstrap/AvatarTrait.php b/tests/integration/features/bootstrap/AvatarTrait.php new file mode 100644 index 00000000000..8edc6146711 --- /dev/null +++ b/tests/integration/features/bootstrap/AvatarTrait.php @@ -0,0 +1,321 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +trait AvatarTrait { + + /** @var string **/ + private $lastAvatar; + + /** @AfterScenario **/ + public function cleanupLastAvatar() { + $this->lastAvatar = null; + } + + private function getLastAvatar() { + $this->lastAvatar = ''; + + $body = $this->response->getBody(); + while (!$body->eof()) { + $this->lastAvatar .= $body->read(8192); + } + $body->close(); + } + /** + * @When user :user gets avatar for room :identifier + * + * @param string $user + * @param string $identifier + */ + public function userGetsAvatarForRoom(string $user, string $identifier) { + $this->userGetsAvatarForRoomWithSize($user, $identifier, '128'); + } + + /** + * @When user :user gets avatar for room :identifier with size :size + * + * @param string $user + * @param string $identifier + * @param string $size + */ + public function userGetsAvatarForRoomWithSize(string $user, string $identifier, string $size) { + $this->userGetsAvatarForRoomWithSizeWith($user, $identifier, $size, '200'); + } + + /** + * @When user :user gets avatar for room :identifier with size :size with :statusCode + * + * @param string $user + * @param string $identifier + * @param string $size + * @param string $statusCode + */ + public function userGetsAvatarForRoomWithSizeWith(string $user, string $identifier, string $size, string $statusCode) { + $this->setCurrentUser($user); + $this->sendRequest('GET', '/apps/spreed/api/v3/avatar/' . FeatureContext::getTokenForIdentifier($identifier) . '/' . $size, null); + $this->assertStatusCode($this->response, $statusCode); + + if ($statusCode !== '200') { + return; + } + + $this->getLastAvatar(); + } + + /** + * @When user :user sets avatar for room :identifier from file :source + * + * @param string $user + * @param string $identifier + * @param string $source + */ + public function userSetsAvatarForRoomFromFile(string $user, string $identifier, string $source) { + $this->userSetsAvatarForRoomFromFileWith($user, $identifier, $source, '200'); + } + + /** + * @When user :user sets avatar for room :identifier from file :source with :statusCode + * + * @param string $user + * @param string $identifier + * @param string $source + * @param string $statusCode + */ + public function userSetsAvatarForRoomFromFileWith(string $user, string $identifier, string $source, string $statusCode) { + $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r')); + + $this->setCurrentUser($user); + $this->sendRequest('POST', '/apps/spreed/api/v3/avatar/' . FeatureContext::getTokenForIdentifier($identifier), + [ + 'multipart' => [ + [ + 'name' => 'files[]', + 'contents' => $file + ] + ] + ]); + $this->assertStatusCode($this->response, $statusCode); + } + + /** + * @When user :user deletes avatar for room :identifier + * + * @param string $user + * @param string $identifier + */ + public function userDeletesAvatarForRoom(string $user, string $identifier) { + $this->userDeletesAvatarForRoomWith($user, $identifier, '200'); + } + + /** + * @When user :user deletes avatar for room :identifier with :statusCode + * + * @param string $user + * @param string $identifier + * @param string $statusCode + */ + public function userDeletesAvatarForRoomWith(string $user, string $identifier, string $statusCode) { + $this->setCurrentUser($user); + $this->sendRequest('DELETE', '/apps/spreed/api/v3/avatar/' . FeatureContext::getTokenForIdentifier($identifier), null); + $this->assertStatusCode($this->response, $statusCode); + } + + /** + * @When logged in user posts temporary avatar from file :source + * + * @param string $source + */ + public function loggedInUserPostsTemporaryAvatarFromFile(string $source) { + $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r')); + + $this->sendingToWithRequestToken('POST', '/index.php/avatar', + [ + 'multipart' => [ + [ + 'name' => 'files[]', + 'contents' => $file + ] + ] + ]); + $this->assertStatusCode($this->response, '200'); + } + + /** + * @When logged in user crops temporary avatar + * + * @param TableNode $crop + */ + public function loggedInUserCropsTemporaryAvatar(TableNode $crop) { + $parameters = []; + foreach ($crop->getRowsHash() as $key => $value) { + $parameters[] = 'crop[' . $key . ']=' . $value; + } + + $this->sendingToWithRequestToken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters)); + $this->assertStatusCode($this->response, '200'); + } + + /** + * @When logged in user deletes the user avatar + */ + public function loggedInUserDeletesTheUserAvatar() { + $this->sendingToWithRequesttoken('DELETE', '/index.php/avatar'); + $this->assertStatusCode($this->response, '200'); + } + + /** + * @Then last avatar is a default avatar of size :size + * + * @param string size + */ + public function lastAvatarIsADefaultAvatarOfSize(string $size) { + $this->theFollowingHeadersShouldBeSet(new TableNode([ + [ 'Content-Type', 'image/png' ], + [ 'X-NC-IsCustomAvatar', '0' ] + ])); + $this->lastAvatarIsASquareOfSize($size); + $this->lastAvatarIsNotASingleColor(); + } + + /** + * @Then last avatar is a custom avatar of size :size and color :color + * + * @param string size + */ + public function lastAvatarIsACustomAvatarOfSizeAndColor(string $size, string $color) { + $this->theFollowingHeadersShouldBeSet(new TableNode([ + [ 'Content-Type', 'image/png' ], + [ 'X-NC-IsCustomAvatar', '1' ] + ])); + $this->lastAvatarIsASquareOfSize($size); + $this->lastAvatarIsASingleColor($color); + } + + /** + * @Then last avatar is a square of size :size + * + * @param string size + */ + public function lastAvatarIsASquareOfSize(string $size) { + list($width, $height) = getimagesizefromstring($this->lastAvatar); + + Assert::assertEquals($width, $height, 'Avatar is not a square'); + Assert::assertEquals($size, $width); + } + + /** + * @Then last avatar is not a single color + */ + public function lastAvatarIsNotASingleColor() { + Assert::assertEquals(null, $this->getColorFromLastAvatar()); + } + + /** + * @Then last avatar is a single :color color + * + * @param string $color + * @param string $size + */ + public function lastAvatarIsASingleColor(string $color) { + $expectedColor = $this->hexStringToRgbColor($color); + $colorFromLastAvatar = $this->getColorFromLastAvatar(); + + if (!$colorFromLastAvatar) { + Assert::fail('Last avatar is not a single color'); + } + + Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar), + $this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color); + } + + private function hexStringToRgbColor($hexString) { + // Strip initial "#" + $hexString = substr($hexString, 1); + + $rgbColorInt = hexdec($hexString); + + // RGBA hex strings are not supported; the given string is assumed to be + // an RGB hex string. + return [ + 'red' => ($rgbColorInt >> 16) & 0xFF, + 'green' => ($rgbColorInt >> 8) & 0xFF, + 'blue' => $rgbColorInt & 0xFF, + 'alpha' => 0 + ]; + } + + private function rgbColorToHexString($rgbColor) { + $rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']); + + return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT); + } + + private function getColorFromLastAvatar() { + $image = imagecreatefromstring($this->lastAvatar); + + $firstPixelColorIndex = imagecolorat($image, 0, 0); + $firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex); + + for ($i = 0; $i < imagesx($image); $i++) { + for ($j = 0; $j < imagesx($image); $j++) { + $currentPixelColorIndex = imagecolorat($image, $i, $j); + $currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex); + + // The colors are compared with a small allowed delta, as even + // on solid color images the resizing can cause some small + // artifacts that slightly modify the color of certain pixels. + if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) { + imagedestroy($image); + + return null; + } + } + } + + imagedestroy($image); + + return $firstPixelColor; + } + + private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && + $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && + $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && + $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + return true; + } + + return false; + } + + private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && + $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + return true; + } + + return false; + } +} diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index ba930a1956d..0c5171d4c34 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -75,12 +75,18 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** @var string */ protected $currentUser; + /** @var string */ + protected $loggedInUser; + /** @var ResponseInterface */ private $response; /** @var CookieJar[] */ private $cookieJars; + /** @var string */ + private $requestToken; + /** @var string */ protected $baseUrl; @@ -111,6 +117,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** @var string */ private $guestsOldWhitelist; + use AvatarTrait; use CommandLineTrait; public static function getTokenForIdentifier(string $identifier) { @@ -315,6 +322,12 @@ private function assertRooms($rooms, TableNode $formData, bool $shouldOrder = fa if (isset($expectedRoom['description'])) { $data['description'] = $room['description']; } + if (isset($expectedRoom['avatarId'])) { + $data['avatarId'] = $room['avatarId']; + } + if (isset($expectedRoom['avatarVersion'])) { + $data['avatarVersion'] = $room['avatarVersion']; + } if (isset($expectedRoom['type'])) { $data['type'] = (string) $room['type']; } @@ -2636,7 +2649,7 @@ public function userLogsIn(string $user) { ] ); - $requestToken = $this->extractRequestTokenFromResponse($this->response); + $this->extractRequestTokenFromResponse($this->response); // Login and extract new token $password = ($user === 'admin') ? 'admin' : self::TEST_PASSWORD; @@ -2647,21 +2660,58 @@ public function userLogsIn(string $user) { 'form_params' => [ 'user' => $user, 'password' => $password, - 'requesttoken' => $requestToken, + 'requesttoken' => $this->requestToken, ], 'cookies' => $cookieJar, ] ); + $this->extractRequestTokenFromResponse($this->response); $this->assertStatusCode($this->response, 200); + + $this->loggedInUser = $user; } /** * @param ResponseInterface $response - * @return string */ - private function extractRequestTokenFromResponse(ResponseInterface $response): string { - return substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89); + private function extractRequestTokenFromResponse(ResponseInterface $response): void { + $this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89); + } + + /** + * @When /^sending "([^"]*)" to "([^"]*)" with request token$/ + * @param string $verb + * @param string $url + * @param TableNode|array|null $body + */ + public function sendingToWithRequestToken(string $verb, string $url, $body = null) { + $fullUrl = $this->baseUrl . $url; + + $options = [ + 'cookies' => $this->getUserCookieJar($this->loggedInUser), + 'headers' => [ + 'requesttoken' => $this->requestToken + ], + ]; + + if ($body instanceof TableNode) { + $fd = $body->getRowsHash(); + $options['form_params'] = $fd; + } elseif ($body) { + $options = array_merge($options, $body); + } + + $client = new Client(); + try { + $this->response = $client->request( + $verb, + $fullUrl, + $options + ); + } catch (ClientException $e) { + $this->response = $e->getResponse(); + } } /** @@ -2704,6 +2754,8 @@ public function sendRequestFullUrl($verb, $fullUrl, $body = null, array $headers if ($body instanceof TableNode) { $fd = $body->getRowsHash(); $options['form_params'] = $fd; + } elseif (is_array($body) && array_key_exists('multipart', $body)) { + $options = array_merge($options, $body); } elseif (is_array($body)) { $options['form_params'] = $body; } @@ -2737,4 +2789,27 @@ protected function getUserCookieJar($user) { protected function assertStatusCode(ResponseInterface $response, int $statusCode, string $message = '') { Assert::assertEquals($statusCode, $response->getStatusCode(), $message); } + + /** + * @Then /^the following headers should be set$/ + * @param TableNode $table + * @throws \Exception + */ + public function theFollowingHeadersShouldBeSet(TableNode $table) { + foreach ($table->getTable() as $header) { + $headerName = $header[0]; + $expectedHeaderValue = $header[1]; + $returnedHeader = $this->response->getHeader($headerName)[0]; + if ($returnedHeader !== $expectedHeaderValue) { + throw new \Exception( + sprintf( + "Expected value '%s' for header '%s', got '%s'", + $expectedHeaderValue, + $headerName, + $returnedHeader + ) + ); + } + } + } } diff --git a/tests/integration/features/chat/system-messages.feature b/tests/integration/features/chat/system-messages.feature index b4ad54865d3..aef9553efd1 100644 --- a/tests/integration/features/chat/system-messages.feature +++ b/tests/integration/features/chat/system-messages.feature @@ -47,6 +47,48 @@ Feature: System messages | room | users | participant1 | participant1-displayname | description_set | | room | users | participant1 | participant1-displayname | conversation_created | + Scenario: Set an avatar + Given user "participant1" creates room "room" + | roomType | 2 | + | roomName | room | + When user "participant1" sets avatar for room "room" from file "data/green-square-256.png" + Then user "participant1" sees the following system messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | systemMessage | + | room | users | participant1 | participant1-displayname | avatar_set | + | room | users | participant1 | participant1-displayname | conversation_created | + + Scenario: Set user avatar of a one-to-one conversation participant + Given user "participant1" creates room "room" + | roomType | 1 | + | invite | participant2 | + When user "participant1" logs in + And logged in user posts temporary avatar from file "data/green-square-256.png" + And logged in user crops temporary avatar + | x | 0 | + | y | 0 | + | w | 256 | + | h | 256 | + # Although the room avatar changes for the other participant no system + # message should be added + Then user "participant1" sees the following system messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | systemMessage | + | room | users | participant1 | participant1-displayname | conversation_created | + And user "participant2" sees the following system messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | systemMessage | + | room | users | participant1 | participant1-displayname | conversation_created | + + Scenario: Removes an avatar + Given user "participant1" creates room "room" + | roomType | 2 | + | roomName | room | + And user "participant1" sets avatar for room "room" from file "data/green-square-256.png" + When user "participant1" deletes avatar for room "room" + Then user "participant1" sees the following system messages in room "room" with 200 + | room | actorType | actorId | actorDisplayName | systemMessage | + | room | users | participant1 | participant1-displayname | avatar_removed | + | room | users | participant1 | participant1-displayname | avatar_set | + | room | users | participant1 | participant1-displayname | conversation_created | + Scenario: Toggle guests Given user "participant1" creates room "room" (v4) | roomType | 2 | diff --git a/tests/integration/features/conversation/avatar.feature b/tests/integration/features/conversation/avatar.feature new file mode 100644 index 00000000000..5c5f67987c2 --- /dev/null +++ b/tests/integration/features/conversation/avatar.feature @@ -0,0 +1,586 @@ +Feature: avatar + + Background: + Given user "owner" exists + Given user "moderator" exists + Given user "invited user" exists + Given user "not invited user" exists + Given user "not invited but joined user" exists + Given user "not joined user" exists + + Scenario: participants can not set avatar in one-to-one room + Given user "owner" creates room "one-to-one room" + | roomType | 1 | + | invite | moderator | + When user "owner" sets avatar for room "one-to-one room" from file "data/green-square-256.png" with 404 + And user "moderator" sets avatar for room "one-to-one room" from file "data/green-square-256.png" with 404 + And user "not invited user" sets avatar for room "one-to-one room" from file "data/green-square-256.png" with 404 + And user "guest" sets avatar for room "one-to-one room" from file "data/green-square-256.png" with 404 + Then user "owner" gets avatar for room "one-to-one room" + And last avatar is a default avatar of size "128" + And user "moderator" gets avatar for room "one-to-one room" + And last avatar is a default avatar of size "128" + + + + Scenario: owner can set avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + When user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + Scenario: moderator can set avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + When user "moderator" sets avatar for room "group room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + Scenario: others can not set avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + When user "invited user" sets avatar for room "group room" from file "data/green-square-256.png" with 404 + And user "not invited user" sets avatar for room "group room" from file "data/green-square-256.png" with 404 + # Guest user names in tests must being with "guest" + And user "guest not joined" sets avatar for room "group room" from file "data/green-square-256.png" with 404 + Then user "owner" gets avatar for room "group room" with size "256" with 404 + And user "moderator" gets avatar for room "group room" with size "256" with 404 + And user "invited user" gets avatar for room "group room" with size "256" with 404 + + + + Scenario: owner can set avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + When user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited but joined user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + Scenario: moderator can set avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + When user "moderator" sets avatar for room "public room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited but joined user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + Scenario: guest moderator can set avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + When user "guest moderator" sets avatar for room "public room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited but joined user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + Scenario: others can not set avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + When user "invited user" sets avatar for room "public room" from file "data/green-square-256.png" with 404 + And user "not invited but joined user" sets avatar for room "public room" from file "data/green-square-256.png" with 404 + And user "not joined user" sets avatar for room "public room" from file "data/green-square-256.png" with 404 + And user "guest" sets avatar for room "public room" from file "data/green-square-256.png" with 404 + # Guest user names in tests must being with "guest" + And user "guest not joined" sets avatar for room "public room" from file "data/green-square-256.png" with 404 + Then user "owner" gets avatar for room "public room" with size "256" with 404 + And user "moderator" gets avatar for room "public room" with size "256" with 404 + And user "invited user" gets avatar for room "public room" with size "256" with 404 + And user "not invited but joined user" gets avatar for room "public room" with size "256" with 404 + And user "guest moderator" gets avatar for room "public room" with size "256" with 404 + And user "guest" gets avatar for room "public room" with size "256" with 404 + + + + Scenario: owner can set avatar in listable room + Given user "owner" creates room "listable room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "listable room" with 200 + And user "owner" promotes "moderator" in room "listable room" with 200 + And user "owner" adds "invited user" to room "listable room" with 200 + And user "owner" allows listing room "listable room" for "users" with 200 + When user "owner" sets avatar for room "listable room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited user" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest not joined" gets avatar for room "listable room" with size "256" with 404 + + Scenario: moderator can set avatar in listable room + Given user "owner" creates room "listable room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "listable room" with 200 + And user "owner" promotes "moderator" in room "listable room" with 200 + And user "owner" adds "invited user" to room "listable room" with 200 + And user "owner" allows listing room "listable room" for "users" with 200 + When user "moderator" sets avatar for room "listable room" from file "data/green-square-256.png" + Then user "owner" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited user" gets avatar for room "listable room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest not joined" gets avatar for room "listable room" with size "256" with 404 + + Scenario: others can not set avatar in listable room + Given user "owner" creates room "listable room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "listable room" with 200 + And user "owner" promotes "moderator" in room "listable room" with 200 + And user "owner" adds "invited user" to room "listable room" with 200 + And user "owner" allows listing room "listable room" for "users" with 200 + When user "invited user" sets avatar for room "listable room" from file "data/green-square-256.png" with 404 + And user "not invited user" sets avatar for room "listable room" from file "data/green-square-256.png" with 404 + # Guest user names in tests must being with "guest" + And user "guest not joined" sets avatar for room "listable room" from file "data/green-square-256.png" with 404 + Then user "owner" gets avatar for room "listable room" with size "256" with 404 + And user "moderator" gets avatar for room "listable room" with size "256" with 404 + And user "invited user" gets avatar for room "listable room" with size "256" with 404 + And user "not invited user" gets avatar for room "listable room" with size "256" with 404 + And user "guest not joined" gets avatar for room "listable room" with size "256" with 404 + + + + Scenario: participants can not set avatar in room for a share + # These users are only needed in very specific tests, so they are not + # created in the background step. + Given user "owner of file" exists + And user "user with access to file" exists + And user "owner of file" shares "welcome.txt" with user "user with access to file" with OCS 100 + And user "user with access to file" accepts last share + And user "owner of file" shares "welcome.txt" by link with OCS 100 + And user "guest" gets the room for last share with 200 + And user "owner of file" joins room "file last share room" with 200 + And user "user with access to file" joins room "file last share room" with 200 + And user "guest" joins room "file last share room" with 200 + When user "owner of file" sets avatar for room "file last share room" from file "data/green-square-256.png" with 404 + And user "user with access to file" sets avatar for room "file last share room" from file "data/green-square-256.png" with 404 + And user "guest" sets avatar for room "file last share room" from file "data/green-square-256.png" with 404 + Then user "owner of file" gets avatar for room "file last share room" with size "256" with 404 + And user "user with access to file" gets avatar for room "file last share room" with size "256" with 404 + And user "guest" gets avatar for room "file last share room" with size "256" with 404 + + + + Scenario: participants can not set avatar in a password request room + # The user is only needed in very specific tests, so it is not created in + # the background step. + Given user "owner of file" exists + And user "owner of file" shares "welcome.txt" by link with OCS 100 + | password | 123456 | + | sendPasswordByTalk | true | + And user "guest" creates the password request room for last share with 201 + And user "guest" joins room "password request for last share room" with 200 + And user "owner of file" joins room "password request for last share room" with 200 + When user "owner of file" sets avatar for room "password request for last share room" from file "data/green-square-256.png" with 404 + And user "guest" sets avatar for room "password request for last share room" from file "data/green-square-256.png" with 404 + Then user "owner of file" gets avatar for room "password request for last share room" with size "256" with 404 + And user "guest" gets avatar for room "password request for last share room" with size "256" with 404 + + + + Scenario: set jpg image as room avatar + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + When user "owner" sets avatar for room "group room" from file "data/blue-square-256.jpg" + Then user "owner" gets avatar for room "group room" with size "256" + And the following headers should be set + | Content-Type | image/jpeg | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size "256" + And last avatar is a single "#0000FF" color + + Scenario: set non squared image as room avatar + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + When user "owner" sets avatar for room "group room" from file "data/green-rectangle-256-128.png" with 400 + Then user "owner" gets avatar for room "group room" with size "256" with 404 + + Scenario: set not an image as room avatar + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + When user "owner" sets avatar for room "group room" from file "data/textfile.txt" with 400 + Then user "owner" gets avatar for room "group room" with size "256" with 404 + + + + Scenario: owner can delete avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + And user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + And user "owner" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + When user "owner" deletes avatar for room "group room" + Then user "owner" gets avatar for room "group room" with size "256" with 404 + And user "moderator" gets avatar for room "group room" with size "256" with 404 + And user "invited user" gets avatar for room "group room" with size "256" with 404 + + Scenario: moderator can delete avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + And user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + And user "owner" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + When user "moderator" deletes avatar for room "group room" + Then user "owner" gets avatar for room "group room" with size "256" with 404 + And user "moderator" gets avatar for room "group room" with size "256" with 404 + And user "invited user" gets avatar for room "group room" with size "256" with 404 + + Scenario: others can not delete avatar in group room + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" adds "moderator" to room "group room" with 200 + And user "owner" promotes "moderator" in room "group room" with 200 + And user "owner" adds "invited user" to room "group room" with 200 + And user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + When user "invited user" deletes avatar for room "group room" with 404 + And user "not invited user" deletes avatar for room "group room" with 404 + # Guest user names in tests must being with "guest" + And user "guest not joined" deletes avatar for room "group room" with 404 + Then user "owner" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "group room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + + + Scenario: owner can delete avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + And user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + And user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + When user "owner" deletes avatar for room "public room" + Then user "owner" gets avatar for room "public room" with size "256" with 404 + And user "moderator" gets avatar for room "public room" with size "256" with 404 + And user "invited user" gets avatar for room "public room" with size "256" with 404 + And user "not invited but joined user" gets avatar for room "public room" with size "256" with 404 + And user "guest moderator" gets avatar for room "public room" with size "256" with 404 + And user "guest" gets avatar for room "public room" with size "256" with 404 + + Scenario: moderator can delete avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + And user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + And user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + When user "moderator" deletes avatar for room "public room" + Then user "owner" gets avatar for room "public room" with size "256" with 404 + And user "moderator" gets avatar for room "public room" with size "256" with 404 + And user "invited user" gets avatar for room "public room" with size "256" with 404 + And user "not invited but joined user" gets avatar for room "public room" with size "256" with 404 + And user "guest moderator" gets avatar for room "public room" with size "256" with 404 + And user "guest" gets avatar for room "public room" with size "256" with 404 + + Scenario: guest moderator can delete avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + And user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + And user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + When user "guest moderator" deletes avatar for room "public room" + Then user "owner" gets avatar for room "public room" with size "256" with 404 + And user "moderator" gets avatar for room "public room" with size "256" with 404 + And user "invited user" gets avatar for room "public room" with size "256" with 404 + And user "not invited but joined user" gets avatar for room "public room" with size "256" with 404 + And user "guest moderator" gets avatar for room "public room" with size "256" with 404 + And user "guest" gets avatar for room "public room" with size "256" with 404 + + Scenario: others can not delete avatar in public room + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "guest moderator" joins room "public room" with 200 + And user "owner" promotes "guest moderator" in room "public room" with 200 + And user "guest" joins room "public room" with 200 + And user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + When user "invited user" deletes avatar for room "public room" with 404 + And user "not invited but joined user" deletes avatar for room "public room" with 404 + And user "not joined user" deletes avatar for room "public room" with 404 + And user "guest" deletes avatar for room "public room" with 404 + # Guest user names in tests must being with "guest" + And user "guest not joined" deletes avatar for room "public room" with 404 + Then user "owner" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "invited user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "not invited but joined user" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest moderator" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + And user "guest" gets avatar for room "public room" with size "256" + And last avatar is a custom avatar of size "256" and color "#00FF00" + + + + Scenario: get room avatar with a larger size than the original one + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + When user "owner" gets avatar for room "group room" with size "512" + Then last avatar is a custom avatar of size "512" and color "#00FF00" + + Scenario: get room avatar with a smaller size than the original one + Given user "owner" creates room "group room" + | roomType | 2 | + | roomName | room | + And user "owner" sets avatar for room "group room" from file "data/green-square-256.png" + When user "owner" gets avatar for room "group room" with size "128" + Then last avatar is a custom avatar of size "128" and color "#00FF00" + + + + Scenario: room list returns the default avatar after room creation + When user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + Then user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 1 | + And user "moderator" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 1 | + And user "invited user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 1 | + And user "not invited but joined user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 1 | + + Scenario: room list returns a custom avatar after avatar is set + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + When user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + Then user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | custom | 2 | + And user "moderator" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | custom | 2 | + And user "invited user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | custom | 2 | + And user "not invited but joined user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | custom | 2 | + + Scenario: room list returns a default avatar after avatar is deleted + Given user "owner" creates room "public room" + | roomType | 3 | + | roomName | room | + And user "owner" adds "moderator" to room "public room" with 200 + And user "owner" promotes "moderator" in room "public room" with 200 + And user "owner" adds "invited user" to room "public room" with 200 + And user "not invited but joined user" joins room "public room" with 200 + And user "owner" sets avatar for room "public room" from file "data/green-square-256.png" + And user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | custom | 2 | + When user "owner" deletes avatar for room "public room" + Then user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 3 | + And user "moderator" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 3 | + And user "invited user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 3 | + And user "not invited but joined user" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | icon-public | 3 | + + + + Scenario: one-to-one room avatar is updated when user avatar is updated + Given user "owner" creates room "one-to-one room" + | roomType | 1 | + | invite | moderator | + When user "owner" logs in + And logged in user posts temporary avatar from file "data/green-square-256.png" + And logged in user crops temporary avatar + | x | 0 | + | y | 0 | + | w | 256 | + | h | 256 | + Then user "owner" gets avatar for room "one-to-one room" with size "256" + And last avatar is a default avatar of size "256" + And user "moderator" gets avatar for room "one-to-one room" with size "256" + # Although the user avatar is a custom avatar the room avatar is still a + # default avatar. + And the following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size "256" + And last avatar is a single "#00FF00" color + And user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | user | 2 | + And user "moderator" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | user | 2 | + + Scenario: one-to-one room avatar is updated when user avatar is deleted + Given user "owner" creates room "one-to-one room" + | roomType | 1 | + | invite | moderator | + And user "owner" logs in + And logged in user posts temporary avatar from file "data/green-square-256.png" + And logged in user crops temporary avatar + | x | 0 | + | y | 0 | + | w | 256 | + | h | 256 | + When logged in user deletes the user avatar + Then user "owner" gets avatar for room "one-to-one room" + And last avatar is a default avatar of size "128" + And user "moderator" gets avatar for room "one-to-one room" + And last avatar is a default avatar of size "128" + And user "owner" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | user | 3 | + And user "moderator" is participant of the following rooms (v3) + | avatarId | avatarVersion | + | user | 3 | diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index 3c050c0899d..edebd7b5ba6 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -122,6 +122,7 @@ public function setUp(): void { 'silent-call', 'send-call-notification', 'talk-polls', + 'room-avatar', 'message-expiration', 'reactions', ]; diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php index ec5f5543b2d..3bb4fa728c4 100644 --- a/tests/php/Chat/Parser/SystemMessageTest.php +++ b/tests/php/Chat/Parser/SystemMessageTest.php @@ -172,6 +172,22 @@ public function dataParseMessage(): array { 'You removed the description', ['actor' => ['id' => 'actor', 'type' => 'user']], ], + ['avatar_set', [], 'recipient', + '{actor} set the conversation picture', + ['actor' => ['id' => 'actor', 'type' => 'user']], + ], + ['avatar_set', [], 'actor', + 'You set the conversation picture', + ['actor' => ['id' => 'actor', 'type' => 'user']], + ], + ['avatar_removed', [], 'recipient', + '{actor} removed the conversation picture', + ['actor' => ['id' => 'actor', 'type' => 'user']], + ], + ['avatar_removed', [], 'actor', + 'You removed the conversation picture', + ['actor' => ['id' => 'actor', 'type' => 'user']], + ], ['call_started', [], 'recipient', '{actor} started a call', ['actor' => ['id' => 'actor', 'type' => 'user']], diff --git a/tests/php/Service/RoomServiceTest.php b/tests/php/Service/RoomServiceTest.php index eff984f37fd..ca228b22cff 100644 --- a/tests/php/Service/RoomServiceTest.php +++ b/tests/php/Service/RoomServiceTest.php @@ -371,6 +371,8 @@ public function testVerifyPassword(): void { 'foobar', 'Test', 'description', + 'avatar-id', + 1, 'passy', '', '', diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php index e450d18f2cc..89a373fda18 100644 --- a/tests/php/Signaling/BackendNotifierTest.php +++ b/tests/php/Signaling/BackendNotifierTest.php @@ -287,6 +287,8 @@ public function testRoomInvite() { 'active-since' => null, 'sip-enabled' => 0, 'participant-list' => 'refresh', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -324,6 +326,8 @@ public function testRoomDisinvite() { 'active-since' => null, 'sip-enabled' => 0, 'participant-list' => 'refresh', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -391,6 +395,8 @@ public function testRoomDisinviteOnLeaveOfNormalUserWithDuplicatedSession() { 'active-since' => null, 'sip-enabled' => 0, 'participant-list' => 'refresh', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -430,6 +436,8 @@ public function testRoomDisinviteOnLeaveOfSelfJoinedUser() { 'active-since' => null, 'sip-enabled' => 0, 'participant-list' => 'refresh', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -463,6 +471,8 @@ public function testRoomDisinviteOnLeaveOfGuest() { 'active-since' => null, 'sip-enabled' => 0, 'participant-list' => 'refresh', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -480,6 +490,8 @@ public function testRoomNameChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, @@ -487,6 +499,8 @@ public function testRoomNameChanged() { 'listable' => Room::LISTABLE_NONE, 'active-since' => null, 'sip-enabled' => 0, + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -504,6 +518,34 @@ public function testRoomDescriptionChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => 'The description', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, + 'type' => $room->getType(), + 'lobby-state' => Webinary::LOBBY_NONE, + 'lobby-timer' => null, + 'read-only' => Room::READ_WRITE, + 'listable' => Room::LISTABLE_NONE, + 'active-since' => null, + 'sip-enabled' => 0, + ], + ], + ]); + } + + public function testRoomAvatarChanged() { + $room = $this->manager->createRoom(Room::PUBLIC_CALL); + $room->setAvatar('avatar-id', 42); + + $this->assertMessageWasSent($room, [ + 'type' => 'update', + 'update' => [ + 'userids' => [ + ], + 'properties' => [ + 'name' => $room->getDisplayName(''), + 'description' => '', + 'avatarId' => 'avatar-id', + 'avatarVersion' => 42, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, @@ -528,6 +570,8 @@ public function testRoomPasswordChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, @@ -552,6 +596,8 @@ public function testRoomTypeChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, @@ -576,6 +622,8 @@ public function testRoomReadOnlyChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, @@ -607,6 +655,8 @@ public function testRoomListableChanged() { 'active-since' => null, 'sip-enabled' => 0, 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, ], ], ]); @@ -624,6 +674,8 @@ public function testRoomLobbyStateChanged() { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NON_MODERATORS, 'lobby-timer' => null, @@ -845,6 +897,8 @@ public function testRoomPropertiesEvent(): void { 'properties' => [ 'name' => $room->getDisplayName(''), 'description' => '', + 'avatarId' => 'icon-public', + 'avatarVersion' => 1, 'type' => $room->getType(), 'lobby-state' => Webinary::LOBBY_NONE, 'lobby-timer' => null, diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 0b509c12754..aca7b4052cc 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -9,6 +9,16 @@ getSettingsManager + + + $listener + + + + + Image + + SchemaWrapper @@ -168,6 +178,11 @@ IRootFolder + + + \OC\Files\Filesystem + + SharedStorage::class