123 lines
7.3 KiB
C++
123 lines
7.3 KiB
C++
#include "groupavatarmanager.h"
|
|
#include "../../db/db_manager.h"
|
|
#include "../../transport/gui_bridge.h"
|
|
#include <QCoreApplication>
|
|
#include <QBuffer>
|
|
#include <QCryptographicHash>
|
|
#include <QImageReader>
|
|
#include <QMetaObject>
|
|
#include <QRandomGenerator>
|
|
#include <QRunnable>
|
|
#include <cstring>
|
|
extern "C" {
|
|
#include "chat/group_avatar.h"
|
|
#include "member_files/protocol.h"
|
|
#include "mem.h"
|
|
#include "debug_config.h"
|
|
}
|
|
static GroupAvatarManager* s_groupAvatars;
|
|
static QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); }
|
|
|
|
GroupAvatarManager::GroupAvatarManager(DbManager* db, QObject* parent) : QObject(parent), m_db(db) {
|
|
s_groupAvatars = this; m_pool.setMaxThreadCount(1);
|
|
}
|
|
GroupAvatarManager::~GroupAvatarManager() { s_groupAvatars = nullptr; m_pool.waitForDone(); }
|
|
GroupAvatarManager* GroupAvatarManager::instance() { return s_groupAvatars; }
|
|
|
|
QImage GroupAvatarManager::currentImage(const QString& channel) const {
|
|
auto* image = m_cache.object(m_current.value(channel)); return image ? *image : QImage();
|
|
}
|
|
QImage GroupAvatarManager::image(const QString& channel) {
|
|
if (!m_db || !m_db->isOpen()) return {};
|
|
for (const auto& row : m_db->getChannels()) if (row.channelId == channel) return image(channel, row.avatarVersion, row.avatarHash);
|
|
return {};
|
|
}
|
|
QImage GroupAvatarManager::image(const QString& channel, quint64 version, const QByteArray& hash) {
|
|
if (!version) { m_current.remove(channel); return {}; }
|
|
const QString key = channel + ':' + QString::number(version) + ':' + QString::fromLatin1(hash.toHex()); m_current[channel] = key;
|
|
if (auto* cached = m_cache.object(key)) return *cached;
|
|
if (m_loading.contains(key) || m_invalid.contains(key) || !m_db || !m_db->isOpen()) return {};
|
|
const QByteArray bytes = m_db->getChannelAvatar(channel); m_loading.insert(key);
|
|
m_pool.start(QRunnable::create([this, key, bytes, hash, channel] {
|
|
QImage image; QString error;
|
|
if (bytes.isEmpty() || bytes.size() > GROUP_AVATAR_MAX || hash.size() != 32 ||
|
|
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256) != hash) error = QStringLiteral("invalid length/hash");
|
|
else {
|
|
QBuffer buffer; buffer.setData(bytes); buffer.open(QIODevice::ReadOnly); QImageReader reader(&buffer, "jpeg");
|
|
QSize size = reader.size();
|
|
if (!size.isValid() || size.width() > 128 || size.height() > 128 || size.width() != size.height()) error = QStringLiteral("invalid dimensions");
|
|
else { image = reader.read(); if (image.isNull()) error = reader.errorString(); }
|
|
}
|
|
QMetaObject::invokeMethod(this, [this, key, image, error, channel, bytes] {
|
|
m_loading.remove(key);
|
|
if (image.isNull()) {
|
|
// Пустое изображение — подписанное удаление, а не ошибка декодирования.
|
|
if (!bytes.isEmpty()) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: decode failed channel=%s reason=%s", qPrintable(channel), qPrintable(error));
|
|
m_invalid.insert(key);
|
|
} else m_cache.insert(key, new QImage(image), qMax(1, int(image.sizeInBytes() / 1024)));
|
|
if (m_current.value(channel) == key) emit changed(channel);
|
|
}, Qt::QueuedConnection);
|
|
}));
|
|
return {};
|
|
}
|
|
|
|
void GroupAvatarManager::upload(const QString& channel, const QString& source) {
|
|
if (!gui_bridge_is_uasync_ready()) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: upload requested while chat is stopped channel=%s", qPrintable(channel));
|
|
emit status(channel, text("Chat is stopped")); return;
|
|
}
|
|
quint64 generation = ++m_generation[channel]; emit status(channel, text("Preparing group avatar…"));
|
|
m_pool.start(QRunnable::create([this, channel, source, generation] {
|
|
QImageReader reader(source); reader.setAutoTransform(true); QImage image; QByteArray encoded; QString error;
|
|
QSize size = reader.size();
|
|
if (!size.isValid() || qint64(size.width()) * size.height() > 16 * 1024 * 1024 || reader.format() == "svg")
|
|
error = QStringLiteral("image exceeds 16 megapixels or has an unsupported format");
|
|
else {
|
|
image = reader.read();
|
|
if (image.isNull()) error = reader.errorString();
|
|
else {
|
|
int side = qMin(image.width(), image.height());
|
|
image = image.copy((image.width() - side) / 2, (image.height() - side) / 2, side, side).convertToFormat(QImage::Format_RGB32);
|
|
for (int dimension : {128, 96, 64}) {
|
|
const QImage thumbnail = image.scaled(dimension, dimension, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
|
for (int quality : {85, 70, 50, 35}) {
|
|
encoded.clear(); QBuffer buffer(&encoded); buffer.open(QIODevice::WriteOnly);
|
|
if (!thumbnail.save(&buffer, "JPEG", quality)) { error = QStringLiteral("JPEG encoder unavailable"); break; }
|
|
if (encoded.size() <= GROUP_AVATAR_MAX) break;
|
|
}
|
|
if (!error.isEmpty() || encoded.size() <= GROUP_AVATAR_MAX) break;
|
|
}
|
|
if (encoded.size() > GROUP_AVATAR_MAX) error = QStringLiteral("cannot fit image in 16 KiB");
|
|
}
|
|
}
|
|
QMetaObject::invokeMethod(this, [this, channel, generation, encoded, error] {
|
|
if (m_generation.value(channel) != generation) return;
|
|
if (!error.isEmpty()) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: preparation failed channel=%s reason=%s", qPrintable(channel), qPrintable(error));
|
|
emit status(channel, text("Cannot prepare avatar: %1").arg(error)); return;
|
|
}
|
|
publish(channel, encoded, generation);
|
|
}, Qt::QueuedConnection);
|
|
}));
|
|
}
|
|
|
|
void GroupAvatarManager::remove(const QString& channel) { publish(channel, {}, ++m_generation[channel]); }
|
|
void GroupAvatarManager::publish(const QString& channel, const QByteArray& bytes, quint64 generation) {
|
|
if (!gui_bridge_is_uasync_ready() || !channel.toULongLong() || bytes.size() > GROUP_AVATAR_MAX) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication unavailable channel=%s", qPrintable(channel));
|
|
emit status(channel, text("Chat is stopped")); return;
|
|
}
|
|
auto* q = (group_avatar_gui_request*)u_calloc(1, sizeof(group_avatar_gui_request));
|
|
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication allocation failed"); emit status(channel, text("Cannot update group avatar")); return; }
|
|
q->inst = gui_bridge_get_inst(); q->group = channel.toULongLong(); q->size = bytes.size();
|
|
do { q->id = QRandomGenerator::global()->generate64(); } while (!q->id || m_requests.contains(q->id));
|
|
memcpy(q->data, bytes.constData(), bytes.size()); m_requests[q->id] = {channel, generation};
|
|
emit status(channel, text("Saving group avatar…")); gui_bridge_post_uasync_fn(group_avatar_gui_set, q);
|
|
}
|
|
void GroupAvatarManager::onResult(const uint8_t* bytes, int length) {
|
|
if (length != 17 || !m_requests.contains(mf_get64(bytes))) return;
|
|
const auto request = m_requests.take(mf_get64(bytes));
|
|
if (request.first.toULongLong() != mf_get64(bytes + 8) || m_generation.value(request.first) != request.second) return;
|
|
emit status(request.first, bytes[16] ? text("Cannot update group avatar") : text("Group avatar updated"));
|
|
}
|