190 lines
6.4 KiB
C++
190 lines
6.4 KiB
C++
#include "avatar_cache.h"
|
|
|
|
#include <GLFW/glfw3.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cctype>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <bcrypt.h>
|
|
#include <objbase.h>
|
|
#include <urlmon.h>
|
|
#include <wincodec.h>
|
|
#endif
|
|
|
|
AvatarCache::AvatarCache(const std::filesystem::path &user_data_directory)
|
|
: directory_(user_data_directory / "avatars")
|
|
{
|
|
std::filesystem::create_directories(directory_);
|
|
}
|
|
|
|
AvatarCache::~AvatarCache()
|
|
{
|
|
shutdown();
|
|
}
|
|
|
|
std::string AvatarCache::hashEmail(const std::string &email) const
|
|
{
|
|
std::string normalized = email;
|
|
normalized.erase(normalized.begin(), std::find_if(normalized.begin(), normalized.end(),
|
|
[](unsigned char value)
|
|
{ return !std::isspace(value); }));
|
|
normalized.erase(std::find_if(normalized.rbegin(), normalized.rend(),
|
|
[](unsigned char value)
|
|
{ return !std::isspace(value); })
|
|
.base(),
|
|
normalized.end());
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
|
[](unsigned char value)
|
|
{ return static_cast<char>(std::tolower(value)); });
|
|
|
|
#ifdef _WIN32
|
|
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
|
BCRYPT_HASH_HANDLE hash = nullptr;
|
|
std::array<unsigned char, 16> digest{};
|
|
DWORD object_size = 0;
|
|
DWORD written = 0;
|
|
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_MD5_ALGORITHM, nullptr, 0) < 0 ||
|
|
BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH, reinterpret_cast<PUCHAR>(&object_size),
|
|
sizeof(object_size), &written, 0) < 0)
|
|
{
|
|
if (algorithm)
|
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
return {};
|
|
}
|
|
std::vector<unsigned char> object(object_size);
|
|
if (BCryptCreateHash(algorithm, &hash, object.data(), object_size, nullptr, 0, 0) < 0 ||
|
|
BCryptHashData(hash, reinterpret_cast<PUCHAR>(normalized.data()),
|
|
static_cast<ULONG>(normalized.size()), 0) < 0 ||
|
|
BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
|
|
{
|
|
if (hash)
|
|
BCryptDestroyHash(hash);
|
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
return {};
|
|
}
|
|
BCryptDestroyHash(hash);
|
|
BCryptCloseAlgorithmProvider(algorithm, 0);
|
|
std::ostringstream output;
|
|
for (unsigned char value : digest)
|
|
output << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(value);
|
|
return output.str();
|
|
#else
|
|
return std::to_string(std::hash<std::string>{}(normalized));
|
|
#endif
|
|
}
|
|
|
|
unsigned int AvatarCache::textureFor(const std::string &email)
|
|
{
|
|
if (email.empty())
|
|
return 0;
|
|
const std::string hash = hashEmail(email);
|
|
if (hash.empty())
|
|
return 0;
|
|
Entry &entry = entries_[hash];
|
|
if (entry.texture)
|
|
return entry.texture;
|
|
if (entry.requested)
|
|
return 0;
|
|
|
|
entry.requested = true;
|
|
entry.file = directory_ / (hash + ".img");
|
|
if (std::filesystem::exists(entry.file))
|
|
{
|
|
entry.texture = loadTexture(entry.file);
|
|
return entry.texture;
|
|
}
|
|
#ifdef _WIN32
|
|
const std::filesystem::path file = entry.file;
|
|
const std::wstring url = L"https://www.gravatar.com/avatar/" +
|
|
std::wstring(hash.begin(), hash.end()) + L"?s=64&d=identicon&r=g";
|
|
entry.download = std::async(std::launch::async, [file, url]
|
|
{ return SUCCEEDED(URLDownloadToFileW(nullptr, url.c_str(), file.wstring().c_str(), 0, nullptr)); });
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
void AvatarCache::update()
|
|
{
|
|
for (auto &[hash, entry] : entries_)
|
|
{
|
|
(void)hash;
|
|
if (entry.texture || !entry.download.valid())
|
|
continue;
|
|
if (entry.download.wait_for(std::chrono::seconds(0)) != std::future_status::ready)
|
|
continue;
|
|
if (entry.download.get())
|
|
entry.texture = loadTexture(entry.file);
|
|
}
|
|
}
|
|
|
|
unsigned int AvatarCache::loadTexture(const std::filesystem::path &file) const
|
|
{
|
|
#ifdef _WIN32
|
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
IWICImagingFactory *factory = nullptr;
|
|
IWICBitmapDecoder *decoder = nullptr;
|
|
IWICBitmapFrameDecode *frame = nullptr;
|
|
IWICFormatConverter *converter = nullptr;
|
|
UINT width = 0;
|
|
UINT height = 0;
|
|
std::vector<unsigned char> pixels;
|
|
unsigned int texture = 0;
|
|
if (SUCCEEDED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
|
|
IID_PPV_ARGS(&factory))) &&
|
|
SUCCEEDED(factory->CreateDecoderFromFilename(file.wstring().c_str(), nullptr, GENERIC_READ,
|
|
WICDecodeMetadataCacheOnLoad, &decoder)) &&
|
|
SUCCEEDED(decoder->GetFrame(0, &frame)) &&
|
|
SUCCEEDED(factory->CreateFormatConverter(&converter)) &&
|
|
SUCCEEDED(converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA,
|
|
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)) &&
|
|
SUCCEEDED(converter->GetSize(&width, &height)))
|
|
{
|
|
pixels.resize(static_cast<size_t>(width) * height * 4);
|
|
if (SUCCEEDED(converter->CopyPixels(nullptr, width * 4,
|
|
static_cast<UINT>(pixels.size()), pixels.data())))
|
|
{
|
|
glGenTextures(1, &texture);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(width), static_cast<int>(height),
|
|
0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
}
|
|
}
|
|
if (converter)
|
|
converter->Release();
|
|
if (frame)
|
|
frame->Release();
|
|
if (decoder)
|
|
decoder->Release();
|
|
if (factory)
|
|
factory->Release();
|
|
CoUninitialize();
|
|
return texture;
|
|
#else
|
|
(void)file;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
void AvatarCache::shutdown()
|
|
{
|
|
for (auto &[hash, entry] : entries_)
|
|
{
|
|
(void)hash;
|
|
if (entry.download.valid())
|
|
entry.download.wait();
|
|
if (entry.texture)
|
|
glDeleteTextures(1, &entry.texture);
|
|
entry.texture = 0;
|
|
}
|
|
entries_.clear();
|
|
}
|