-
-
Notifications
You must be signed in to change notification settings - Fork 317
Revamp avatar system, remove MC avatars from core #3313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
tadhgboyle
wants to merge
27
commits into
develop
Choose a base branch
from
feat/avatar-revamp
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
bf886be
initial commit
tadhgboyle fa80f4b
wip
tadhgboyle e2a928f
wip
tadhgboyle 7ec6d2a
higher res on leaderboard page
tadhgboyle eaa1312
fix missing translation
tadhgboyle 68042fd
auto-detect if uuid or not, fix old references
tadhgboyle a6027de
fix style
tadhgboyle 7540cb1
clear avatar caches
tadhgboyle dedfd23
fix variable
tadhgboyle 7969f87
fix caching
tadhgboyle 6acd7f2
fix email message
tadhgboyle 524a8e0
don't hardcode source names
tadhgboyle 0af2554
use svg for initials when possible
tadhgboyle 6029208
remove unused terms
tadhgboyle 1f8335c
Merge branch 'develop' into feat/avatar-revamp
tadhgboyle 8ebc31a
clear cache
tadhgboyle f9adb31
Merge branch 'develop' into feat/avatar-revamp
tadhgboyle 29e862e
fix merge issues
tadhgboyle 967b783
fix merge issues
tadhgboyle bb8808f
fix merge issues
tadhgboyle b781ee1
wip
tadhgboyle 633d834
wip
tadhgboyle b6395a2
Merge branch 'develop' into feat/avatar-revamp
tadhgboyle 304dbb2
Merge branch 'develop' into feat/avatar-revamp
tadhgboyle b87c366
Merge branch 'develop' into feat/avatar-revamp
tadhgboyle c4392f2
fix forum post CSS + add mention border
tadhgboyle f77e8fa
use `Settings` class
tadhgboyle File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,241 +1,127 @@ | ||
| <?php | ||
| /** | ||
| * Manages avatar sources and provides static methods for fetching avatars. | ||
| * Manages avatar sources and provides methods for fetching avatars. | ||
| * | ||
| * @package NamelessMC\Avatars | ||
| * @author Aberdeener | ||
| * @version 2.0.0-pr10 | ||
| * @license MIT | ||
| */ | ||
| class AvatarSource { | ||
| class AvatarSource extends Instanceable { | ||
|
|
||
| protected static array $_sources = []; | ||
| /** @var AvatarSourceBase[] */ | ||
| protected array $_sources = []; | ||
| private Cache $_cache; | ||
|
|
||
| protected static AvatarSourceBase $_active_source; | ||
|
|
||
| /** | ||
| * Main usage of this class. | ||
| * Uses active avatar source to get the URL of their Minecraft avatar. | ||
| * | ||
| * @param string $uuid UUID of avatar to get. | ||
| * @param int $size Size in pixels to render avatar at. Default 128 | ||
| * | ||
| * @return string Compiled URL of avatar image. | ||
| */ | ||
| public static function getAvatarFromUUID(string $uuid, int $size = 128): string { | ||
| return self::getActiveSource()->getAvatar($uuid, self::getDefaultPerspective(), $size); | ||
| protected function __construct() { | ||
| $this->_cache = new Cache(['name' => 'nameless', 'extension' => '.cache', 'path' => ROOT_PATH . '/cache/']); | ||
| } | ||
|
|
||
| /** | ||
| * Get a user's avatar from their raw data object. | ||
| * Used by the API for TinyMCE mention avatars to avoid reloading the user from the database. | ||
| * | ||
| * @param object $data User data to use | ||
| * @param bool $allow_gifs Whether to allow GIFs or not () | ||
| * @param int $size Size in pixels to render avatar at. Default 128 | ||
| * @param bool $full Whether to return the full URL or just the path | ||
| * Get an avatar URL for a user. | ||
| * | ||
| * @return string Full URL of avatar image. | ||
| * @param int|User $user User to fetch avatar for, or their ID. | ||
| * @param int $size Size of avatar to fetch in pixels. | ||
| * @param bool $full_url Whether to return the full external URL (ie: for display in Discord embed) or just the path. | ||
| * @return string The URL to the avatar. | ||
| */ | ||
| public static function getAvatarFromUserData(object $data, bool $allow_gifs = false, int $size = 128, bool $full = false): string { | ||
| // If custom avatars are enabled, first check if they have gravatar enabled, and then fallback to normal image | ||
| if (defined('CUSTOM_AVATARS')) { | ||
| if ($data->gravatar) { | ||
| return 'https://secure.gravatar.com/avatar/' . md5(strtolower(trim($data->email))) . '?s=' . $size; | ||
| } | ||
|
|
||
| if ($data->has_avatar) { | ||
| $exts = ['png', 'jpg', 'jpeg']; | ||
|
|
||
| if ($allow_gifs) { | ||
| $exts[] = 'gif'; | ||
| } | ||
|
|
||
| foreach ($exts as $ext) { | ||
| if (file_exists(ROOT_PATH . '/uploads/avatars/' . $data->id . '.' . $ext)) { | ||
| // We don't check the validity here since we know the file exists for sure | ||
| return ($full ? rtrim(URL::getSelfURL(), '/') : '') . ((defined('CONFIG_PATH')) ? CONFIG_PATH . '/' : '/') . 'uploads/avatars/' . $data->id . '.' . $ext . '?v=' . urlencode($data->avatar_updated); | ||
| } | ||
| } | ||
| } | ||
| public function getAvatarForUser($user, int $size = 128, bool $full_url = false): string { | ||
| if ($user instanceof User) { | ||
| $user_id = $user->data()->id; | ||
| } else { | ||
| $user_id = (int) $user; | ||
| } | ||
|
|
||
| // Fallback to default avatar image if it is set and the avatar type is custom | ||
| if (defined('DEFAULT_AVATAR_TYPE') && DEFAULT_AVATAR_TYPE == 'custom' && DEFAULT_AVATAR_IMAGE !== '') { | ||
| if (file_exists(ROOT_PATH . '/uploads/avatars/defaults/' . DEFAULT_AVATAR_IMAGE)) { | ||
| // We don't check the validity here since we know the file exists for sure | ||
| return ($full ? rtrim(URL::getSelfURL(), '/') : '') . ((defined('CONFIG_PATH')) ? CONFIG_PATH . '/' : '/') . 'uploads/avatars/defaults/' . DEFAULT_AVATAR_IMAGE; | ||
| $this->_cache->setCache('avatars'); | ||
|
|
||
| foreach ($this->getAllSources() as $source) { | ||
| if (!$source->isEnabled() && $source->canBeDisabled()) { | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| // Attempt to get their MC avatar if Minecraft integration is enabled | ||
| if (Settings::get('mc_integration')) { | ||
| if ($data->uuid != null && $data->uuid != 'none') { | ||
| $uuid = $data->uuid; | ||
| } else { | ||
| $uuid = $data->username; | ||
| // Fallback to steve avatar if they have an invalid username | ||
| if (preg_match('#[^][_A-Za-z0-9]#', $uuid)) { | ||
| $uuid = 'Steve'; | ||
| $cache_key = $user_id . '_' . $source->getSafeName() . '_' . $size . '_' . (int) $full_url; | ||
| if ($this->_cache->isCached($cache_key)) { | ||
| $avatar = $this->_cache->retrieve($cache_key); | ||
| if ($avatar) { | ||
| return $avatar; | ||
| } | ||
| } | ||
|
|
||
| $url = self::getAvatarFromUUID($uuid, $size); | ||
| // The avatar might be invalid if they are using | ||
| // an MC avatar service that uses only UUIDs | ||
| // and this user doesn't have one | ||
| if (self::validImageUrl($url)) { | ||
| return $url; | ||
| if (!($user instanceof User)) { | ||
| $user = new User($user_id); | ||
| } | ||
| } | ||
|
|
||
| return "https://api.dicebear.com/5.x/initials/png?seed={$data->username}&size={$size}"; | ||
| } | ||
|
|
||
| /** | ||
| * Determine if a URL is a valid image URL for avatars. | ||
| * | ||
| * @param string $url URL to check | ||
| * @return bool Whether the URL is a valid image URL | ||
| */ | ||
| private static function validImageUrl(string $url): bool { | ||
| $cache = new Cache(['name' => 'nameless', 'extension' => '.cache', 'path' => ROOT_PATH . '/cache/']); | ||
| $cache->setCache('avatar_validity'); | ||
|
|
||
| if ($cache->isCached($url)) { | ||
| return $cache->retrieve($url); | ||
| } | ||
|
|
||
| $is_valid = false; | ||
| try { | ||
| $response = HttpClient::createClient()->head($url); | ||
| $headers = $response->getHeaders(); | ||
| if (isset($headers['Content-Type']) && $headers['Content-Type'][0] === 'image/png') { | ||
| $is_valid = true; | ||
| $avatar = $source->getAvatar($user, $size, $full_url); | ||
| if ($avatar) { | ||
| $url = $avatar; | ||
| // Cache for an hour incase a module does not reset the users avatar cache | ||
| $this->_cache->store($cache_key, $url, 3600); | ||
| break; | ||
| } | ||
| } catch (Exception $ignored) { | ||
| } | ||
|
|
||
| $cache->store($url, $is_valid, 3600); | ||
| return $is_valid; | ||
| } | ||
| // Fallback to initials avatar | ||
| if (!isset($url)) { | ||
| $url = $this->_sources[InitialsAvatarSource::class]->getAvatar($user, $size, $full_url); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You never store this in cache if it fails, which would mean we attempt to re-fetch it from the respective avatar source every page load until it works. Is this intentional? |
||
| } | ||
|
|
||
| /** | ||
| * Get the currently active avatar source. | ||
| * | ||
| * @return AvatarSourceBase The active source. | ||
| */ | ||
| public static function getActiveSource(): AvatarSourceBase { | ||
| return self::$_active_source; | ||
| return $url; | ||
| } | ||
|
|
||
| /** | ||
| * Set the active source to the source by name. | ||
| * Fallsback to Cravatar if name was not found. | ||
| * | ||
| * @param string $name Name of source to set as active. | ||
| * @param int|User $user | ||
| * @param string|null $source_class | ||
| * @return void | ||
| */ | ||
| public static function setActiveSource(string $name): void { | ||
| $source = self::getSourceByName($name); | ||
| if ($source === null) { | ||
| $source = self::getSourceByName('cravatar'); | ||
| public function clearUserAvatarCache($user, string $source_class = null): void { | ||
| if ($user instanceof User) { | ||
| $user_id = $user->data()->id; | ||
| } else { | ||
| $user_id = (int) $user; | ||
| } | ||
|
|
||
| self::$_active_source = $source; | ||
| } | ||
| $this->_cache->setCache('avatars'); | ||
|
|
||
| /** | ||
| * Get default perspective to pass to the active avatar source. | ||
| * | ||
| * @return string Perspective. | ||
| */ | ||
| private static function getDefaultPerspective(): string { | ||
| if (defined('DEFAULT_AVATAR_PERSPECTIVE')) { | ||
| return DEFAULT_AVATAR_PERSPECTIVE; | ||
| foreach (array_keys($this->_cache->retrieveAll()) as $cache_key) { | ||
| if (str_starts_with($cache_key, $user_id . '_' . ($source_class ?? ''))) { | ||
| $this->_cache->erase($cache_key); | ||
| } | ||
| } | ||
|
|
||
| return 'face'; | ||
| } | ||
|
|
||
| /** | ||
| * Find an avatar source instance by it's name. | ||
| * | ||
| * @return AvatarSourceBase|null Instance if found, null if not found. | ||
| */ | ||
| public static function getSourceByName(string $name): ?AvatarSourceBase { | ||
| foreach (self::getAllSources() as $source) { | ||
| if (strtolower($source->getName()) == strtolower($name)) { | ||
| return $source; | ||
| public function clearSourceAvatarCache(string $source_class): void { | ||
| $this->_cache->setCache('avatars'); | ||
|
|
||
| foreach (array_keys($this->_cache->retrieveAll()) as $cache_key) { | ||
| if (str_contains($cache_key, $source_class)) { | ||
| $this->_cache->erase($cache_key); | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Get all registered sources. | ||
| * | ||
| * @return AvatarSourceBase[] | ||
| */ | ||
| public static function getAllSources(): iterable { | ||
| return self::$_sources; | ||
| public function getAllSources(): array { | ||
| $sources = $this->_sources; | ||
| uasort($sources, static function (AvatarSourceBase $a, AvatarSourceBase $b) { | ||
| return $a->getOrder() - $b->getOrder(); | ||
| }); | ||
| return $sources; | ||
| } | ||
|
|
||
| /** | ||
| * Get raw url of active avatar source with placeholders. | ||
| * | ||
| * @return string URL with placeholders. | ||
| */ | ||
| public static function getUrlToFormat(): string { | ||
| // Default to Cravatar | ||
| if (!isset(self::$_active_source)) { | ||
| require_once(ROOT_PATH . '/modules/Core/classes/Avatars/CravatarAvatarSource.php'); | ||
| return (new CravatarAvatarSource())->getUrlToFormat(self::getDefaultPerspective()); | ||
| } | ||
|
|
||
| return self::getActiveSource()->getUrlToFormat(self::getDefaultPerspective()); | ||
| public function getSourceBySafeName(string $safe_name): ?AvatarSourceBase { | ||
| return $this->_sources[$safe_name] ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Register avatar source. | ||
| * | ||
| * @param AvatarSourceBase $source Instance of avatar source to register. | ||
| */ | ||
| public static function registerSource(AvatarSourceBase $source): void { | ||
| self::$_sources[] = $source; | ||
| } | ||
|
|
||
| /** | ||
| * Get the names and base urls of all the registered avatar sources for displaying. | ||
| * Used for showing list of sources in staffcp. | ||
| * | ||
| * @return array<string, string> List of names. | ||
| */ | ||
| public static function getAllSourceNames(): array { | ||
| $names = []; | ||
|
|
||
| foreach (self::getAllSources() as $source) { | ||
| $names[$source->getName()] = rtrim($source->getBaseUrl(), '/'); | ||
| } | ||
|
|
||
| return $names; | ||
| } | ||
|
|
||
| /** | ||
| * Get key value array of all registered sources and their available perspectives. | ||
| * Used for autoupdating dropdown selector in staffcp. | ||
| * | ||
| * @return array<string, array<string>> Array of source => [] perspectives. | ||
| */ | ||
| public static function getAllPerspectives(): array { | ||
| $perspectives = []; | ||
|
|
||
| foreach (self::getAllSources() as $source) { | ||
| foreach ($source->getPerspectives() as $perspective) { | ||
| $perspectives[$source->getName()][] = $perspective; | ||
| } | ||
| } | ||
|
|
||
| return $perspectives; | ||
| public function registerSource(AvatarSourceBase $source): void { | ||
| $this->_sources[$source->getSafeName()] = $source; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this check a bit redundant? Seeing as you already checked if it's in there?