diff --git a/appinfo/info.xml b/appinfo/info.xml
index 22b94f93af5..6d3d3492c99 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m
]]>
- 11.0.0-alpha.2
+ 11.0.0-alpha.3
agpl
Daniel Calviño Sánchez
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 1c6aed1ad11..ce647f4878c 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -592,5 +592,33 @@
'apiVersion' => 'v1',
],
],
+
+ /**
+ * Room avatar
+ */
+ [
+ 'name' => 'RoomAvatar#getAvatar',
+ 'url' => '/api/{apiVersion}/avatar/{roomToken}/{size}',
+ 'verb' => 'GET',
+ 'requirements' => [
+ 'apiVersion' => 'v3',
+ ],
+ ],
+ [
+ 'name' => 'RoomAvatar#setAvatar',
+ 'url' => '/api/{apiVersion}/avatar/{roomToken}',
+ 'verb' => 'POST',
+ 'requirements' => [
+ 'apiVersion' => 'v3',
+ ],
+ ],
+ [
+ 'name' => 'RoomAvatar#deleteAvatar',
+ 'url' => '/api/{apiVersion}/avatar/{roomToken}',
+ 'verb' => 'DELETE',
+ 'requirements' => [
+ 'apiVersion' => 'v3',
+ ],
+ ],
],
];
diff --git a/docs/capabilities.md b/docs/capabilities.md
index faf3b18d962..3da3721d74c 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -62,5 +62,6 @@ 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 7c3ae7e40af..912a3250560 100644
--- a/docs/conversation.md
+++ b/docs/conversation.md
@@ -48,6 +48,8 @@
`name` | string | * | Name of the conversation (can also be empty)
`displayName` | string | * | `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 | * | 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 787db6da2f3..528d432f537 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -25,6 +25,7 @@
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;
@@ -131,6 +132,7 @@ public function boot(IBootContext $context): void {
ChangelogListener::register($dispatcher);
ShareListener::register($dispatcher);
Operation::register($dispatcher);
+ AvatarListener::register($dispatcher);
$this->registerRoomActivityHooks($dispatcher);
$this->registerChatHooks($dispatcher);
diff --git a/lib/Avatar/Listener.php b/lib/Avatar/Listener.php
new file mode 100644
index 00000000000..4c50ff68c05
--- /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) {
+ 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..3637858356e
--- /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\Files\File;
+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
+ *
+ * @return void
+ */
+ public function remove(): 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
+ * @return ISimpleFile|File
+ * @throws NotFoundException
+ */
+ public function getFile($size) {
+ $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 $text) {
+ // Unused, unneeded, and Color class it not even public, so just return
+ // null.
+ return null;
+ }
+
+ /**
+ * Ignored.
+ */
+ public function userChanged($feature, $oldValue, $newValue) {
+ }
+}
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 1f3a5472080..5a3d46c1c2e 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -87,6 +87,7 @@ public function getCapabilities(): array {
'phonebook-search',
'raise-hand',
'room-description',
+ 'room-avatar',
],
'config' => [
'attachments' => [
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index 9633b957eef..d08bad77224 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -134,6 +134,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 f839196ad0e..f9ecf868852 100644
--- a/lib/Chat/SystemMessage/Listener.php
+++ b/lib/Chat/SystemMessage/Listener.php
@@ -136,6 +136,17 @@ public static function register(IEventDispatcher $dispatcher): void {
$listener->sendSystemMessage($room, 'description_removed');
}
});
+ $dispatcher->addListener(Room::EVENT_AFTER_AVATAR_SET, static function (ModifyRoomEvent $event) {
+ $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');
+ }
+ });
$dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, static function (ModifyRoomEvent $event) {
$room = $event->getRoom();
/** @var self $listener */
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 6371f131e05..0089908688b 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -584,6 +584,8 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan
'canEnableSIP' => false,
'attendeePin' => '',
'description' => '',
+ 'avatarId' => '',
+ 'avatarVersion' => 0,
'lastCommonReadMessage' => 0,
'listable' => Room::LISTABLE_NONE,
]);
@@ -658,6 +660,8 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan
'actorId' => $attendee->getActorId(),
'attendeeId' => $attendee->getId(),
'description' => $room->getDescription(),
+ 'avatarId' => $room->getAvatarId(),
+ 'avatarVersion' => $room->getAvatarVersion(),
'listable' => $room->getListable(),
]);
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 fc396ff56b1..aeb6bc6232d 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;
@@ -185,6 +186,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'],
(int) $row['active_guests'],
$activeSince,
@@ -804,6 +807,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);
+
$query = $this->db->getQueryBuilder();
$query->insert('talk_rooms')
->values(
@@ -811,6 +816,7 @@ public function createRoom(int $type, string $name = '', string $objectType = ''
'name' => $query->createNamedParameter($name),
'type' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT),
'token' => $query->createNamedParameter($token),
+ 'avatar_id' => $query->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 48973cacc36..16372cf42a4 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 6943e69d56c..5beb4525c44 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\ModifyLobbyEvent;
use OCA\Talk\Events\ModifyRoomEvent;
use OCA\Talk\Events\RoomEvent;
@@ -92,6 +93,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';
@@ -165,6 +168,10 @@ class Room {
/** @var string */
private $description;
/** @var string */
+ private $avatarId;
+ /** @var int */
+ private $avatarVersion;
+ /** @var string */
private $password;
/** @var int */
private $activeGuests;
@@ -202,6 +209,8 @@ public function __construct(Manager $manager,
string $token,
string $name,
string $description,
+ string $avatarId,
+ int $avatarVersion,
string $password,
int $activeGuests,
\DateTime $activeSince = null,
@@ -227,6 +236,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->activeGuests = $activeGuests;
$this->activeSince = $activeSince;
@@ -314,6 +325,14 @@ public function getDescription(): string {
return $this->description;
}
+ public function getAvatarId(): string {
+ return $this->avatarId;
+ }
+
+ public function getAvatarVersion(): int {
+ return $this->avatarVersion;
+ }
+
public function getActiveGuests(): int {
return $this->activeGuests;
}
@@ -380,6 +399,8 @@ public function getPropertiesForSignaling(string $userId, bool $roomModified = t
if ($roomModified) {
$properties = array_merge($properties, [
'description' => $this->getDescription(),
+ 'avatarId' => $this->getAvatarId(),
+ 'avatarVersion' => $this->getAvatarVersion(),
]);
}
@@ -618,6 +639,41 @@ public function setDescription(string $description): 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 string $password Currently it is only allowed to have a password for Room::PUBLIC_CALL
* @return bool True when the change was valid, false otherwise
diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php
index d5625d1676a..e4f33afc53a 100644
--- a/lib/Signaling/Listener.php
+++ b/lib/Signaling/Listener.php
@@ -137,6 +137,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher
};
$dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, $listener);
+ $dispatcher->addListener(Room::EVENT_AFTER_AVATAR_SET, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, $listener);
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 8a57ed81399..d86b1e3df24 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -51,12 +51,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;
@@ -84,6 +90,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
/** @var string */
private $guestsOldWhitelist;
+ use AvatarTrait;
use CommandLineTrait;
public static function getTokenForIdentifier(string $identifier) {
@@ -252,6 +259,12 @@ private function assertRooms($rooms, TableNode $formData) {
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'];
}
@@ -1745,7 +1758,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;
@@ -1756,21 +1769,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();
+ }
}
/**
@@ -1792,6 +1842,8 @@ public function sendRequest($verb, $url, $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;
}
@@ -1823,4 +1875,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 453b52c1e60..98247a8a09b 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"
| 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 b53bf418e51..1515811c903 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -84,6 +84,7 @@ public function setUp(): void {
'phonebook-search',
'raise-hand',
'room-description',
+ 'room-avatar',
];
}
diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php
index 772f60b40bf..156d8839630 100644
--- a/tests/php/Chat/Parser/SystemMessageTest.php
+++ b/tests/php/Chat/Parser/SystemMessageTest.php
@@ -153,6 +153,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/RoomTest.php b/tests/php/RoomTest.php
index 5c710c4f46a..7ed0912cd23 100644
--- a/tests/php/RoomTest.php
+++ b/tests/php/RoomTest.php
@@ -70,6 +70,8 @@ public function testVerifyPassword() {
'foobar',
'Test',
'description',
+ 'avatar-id',
+ 1,
'passy',
0,
null,
diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php
index 57226b74312..579fd42ef80 100644
--- a/tests/php/Signaling/BackendNotifierTest.php
+++ b/tests/php/Signaling/BackendNotifierTest.php
@@ -311,6 +311,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,
@@ -335,6 +337,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,
@@ -359,6 +389,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,
@@ -383,6 +415,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,
@@ -407,6 +441,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,
@@ -438,6 +474,8 @@ public function testRoomListableChanged() {
'active-since' => null,
'sip-enabled' => 0,
'description' => '',
+ 'avatarId' => 'icon-public',
+ 'avatarVersion' => 1,
],
],
]);
@@ -455,6 +493,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,
@@ -615,6 +655,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 9fd61c02938..6a525551f55 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -9,6 +9,11 @@
getSettingsManager
+
+
+ $listener
+
+
SchemaWrapper