diff options
author | Joas Schilling <coding@schilljs.com> | 2023-05-04 13:08:16 +0200 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2023-05-04 16:38:59 +0200 |
commit | 555f4a7e3ab4c69be79dffcf8f794ec3bfa2c8b2 (patch) | |
tree | 44a853200d20c79087d8d9893a0e9b47f856bdcf | |
parent | 741f7c5689908742fdd757e68d293b09d1b34a53 (diff) |
feat(avatar): Allow to select an emoji + color as avatar
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r-- | appinfo/routes/routesAvatarController.php | 2 | ||||
-rw-r--r-- | lib/Controller/AvatarController.php | 23 | ||||
-rw-r--r-- | lib/Service/AvatarService.php | 67 |
3 files changed, 86 insertions, 6 deletions
diff --git a/appinfo/routes/routesAvatarController.php b/appinfo/routes/routesAvatarController.php index 99aa7f1a0..adeeff069 100644 --- a/appinfo/routes/routesAvatarController.php +++ b/appinfo/routes/routesAvatarController.php @@ -32,6 +32,8 @@ return [ 'ocs' => [ /** @see \OCA\Talk\Controller\AvatarController::uploadAvatar() */ ['name' => 'Avatar#uploadAvatar', 'url' => '/api/{apiVersion}/room/{token}/avatar', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\AvatarController::emojiAvatar() */ + ['name' => 'Avatar#emojiAvatar', 'url' => '/api/{apiVersion}/room/{token}/avatar/emoji', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\AvatarController::getAvatar() */ ['name' => 'Avatar#getAvatar', 'url' => '/api/{apiVersion}/room/{token}/avatar', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\AvatarController::getAvatarDark() */ diff --git a/lib/Controller/AvatarController.php b/lib/Controller/AvatarController.php index 7edebf977..b069975c0 100644 --- a/lib/Controller/AvatarController.php +++ b/lib/Controller/AvatarController.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OCA\Talk\Controller; use InvalidArgumentException; +use OCA\Mail\Service\Avatar\Avatar; use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Service\AvatarService; @@ -79,6 +80,28 @@ class AvatarController extends AEnvironmentAwareController { } #[PublicPage] + #[RequireModeratorParticipant] + public function emojiAvatar(string $emoji, ?string $color): DataResponse { + try { + $this->avatarService->setAvatarFromEmoji($this->getRoom(), $emoji, $color); + return new DataResponse($this->roomFormatter->formatRoom( + $this->getResponseFormat(), + [], + $this->getRoom(), + $this->participant, + )); + } catch (InvalidArgumentException $e) { + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } catch (\Exception $e) { + $this->logger->error('Failed to post avatar', [ + 'exception' => $e, + ]); + + return new DataResponse(['message' => $this->l->t('An error occurred. Please contact your administrator.')], Http::STATUS_BAD_REQUEST); + } + } + + #[PublicPage] #[NoCSRFRequired] #[RequireParticipant] public function getAvatar(bool $darkTheme = false): Response { diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index b6a727dc4..02871f321 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -42,6 +42,10 @@ use OCP\IUser; use OCP\Security\ISecureRandom; class AvatarService { + public const THEMING_PLACEHOLDER = '{{THEMING}}'; + public const THEMING_DARK_BACKGROUND = '3B3B3B'; + public const THEMING_BRIGHT_BACKGROUND = 'DBDBDB'; + public function __construct( private IAppData $appData, private IL10N $l, @@ -81,6 +85,36 @@ class AvatarService { $this->setAvatar($room, $image); } + public function setAvatarFromEmoji(Room $room, string $emoji, ?string $color): void { + if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) { + throw new InvalidArgumentException($this->l->t('One-to-one rooms always need to show the other users avatar')); + } + + if ($this->getFirstCombinedEmoji($emoji) !== $emoji) { + throw new InvalidArgumentException($this->l->t('Invalid emoji character')); + } + + if ($color === null) { + $color = self::THEMING_PLACEHOLDER; + } elseif (!preg_match('/^[a-fA-F0-9]{6}$/', $color)) { + throw new InvalidArgumentException($this->l->t('Invalid background color')); + } + + $content = $this->getEmojiAvatar($emoji, $color); + + $token = $room->getToken(); + $avatarFolder = $this->getAvatarFolder($token); + + // Delete previous avatars + foreach ($avatarFolder->getDirectoryListing() as $file) { + $file->delete(); + } + + $avatarName = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE) . '.svg'; + $avatarFolder->newFile($avatarName, $content); + $this->roomService->setAvatar($room, $avatarName); + } + public function setAvatar(Room $room, \OC_Image $image): void { if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) { throw new InvalidArgumentException($this->l->t('One-to-one rooms always need to show the other users avatar')); @@ -164,7 +198,17 @@ class AvatarService { try { $folder = $this->appData->getFolder('room-avatar'); if ($folder->fileExists($token)) { - return $folder->getFolder($token)->getFile($avatar); + $file = $folder->getFolder($token)->getFile($avatar); + + if ($file->getMimeType() === 'image/svg+xml' && str_contains($file->getContent(), self::THEMING_PLACEHOLDER)) { + $color = $darkTheme ? self::THEMING_DARK_BACKGROUND : self::THEMING_BRIGHT_BACKGROUND; + return new InMemoryFile( + $file->getName(), + str_replace(self::THEMING_PLACEHOLDER, $color, $file->getContent()), + ); + } + + return $file; } } catch (NotFoundException $e) { } @@ -181,19 +225,26 @@ class AvatarService { } } if ($this->emojiHelper->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) { - return new InMemoryFile($token, $this->getEmojiAvatar($room->getName(), $darkTheme)); + return new InMemoryFile( + $token, + $this->getEmojiAvatar( + $this->getFirstCombinedEmoji( + $room->getName()), + $darkTheme ? self::THEMING_DARK_BACKGROUND : self::THEMING_BRIGHT_BACKGROUND + ) + ); } return new InMemoryFile($token, file_get_contents($this->getAvatarPath($room, $darkTheme))); } - protected function getEmojiAvatar(string $roomName, bool $darkTheme = false): string { + protected function getEmojiAvatar(string $emoji, ?string $fillColor): string { return str_replace([ '{letter}', '{fill}', '{font}', ], [ - $this->getFirstCombinedEmoji($roomName), - $darkTheme ? '3B3B3B' : 'DBDBDB', + $emoji, + $fillColor, implode(',', [ "'Segoe UI'", 'Roboto', @@ -220,6 +271,10 @@ class AvatarService { * @return string */ protected function getFirstCombinedEmoji(string $roomName, int $length = 0): string { + if (mb_strlen($roomName) === $length) { + return ''; + } + $attempt = mb_substr($roomName, 0, $length + 1); if ($this->emojiHelper->isValidSingleEmoji($attempt)) { $longerAttempt = $this->getFirstCombinedEmoji($roomName, $length + 1); @@ -287,7 +342,7 @@ class AvatarService { return $version; } if ($this->emojiHelper->isValidSingleEmoji(mb_substr($room->getName(), 0, 1))) { - return substr(md5($this->getEmojiAvatar($room->getName())), 0, 8); + return substr(md5($this->getEmojiAvatar($this->getFirstCombinedEmoji($room->getName()), self::THEMING_BRIGHT_BACKGROUND)), 0, 8); } $avatarPath = $this->getAvatarPath($room); return substr(md5($avatarPath), 0, 8); |