diff --git a/tools/chatgui/src/avatars/groupavatarmanager.cpp b/tools/chatgui/src/avatars/groupavatarmanager.cpp new file mode 100644 index 00000000..247c6cea --- /dev/null +++ b/tools/chatgui/src/avatars/groupavatarmanager.cpp @@ -0,0 +1,119 @@ +#include "groupavatarmanager.h" +#include "../../db/db_manager.h" +#include "../../transport/gui_bridge.h" +#include +#include +#include +#include +#include +#include +#include +#include +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()) { 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")); +} diff --git a/tools/chatgui/src/avatars/groupavatarmanager.h b/tools/chatgui/src/avatars/groupavatarmanager.h new file mode 100644 index 00000000..abc86fb2 --- /dev/null +++ b/tools/chatgui/src/avatars/groupavatarmanager.h @@ -0,0 +1,34 @@ +/* SQLite — постоянный кеш; QImage декодируется один раз на подписанную версию. */ +#pragma once +#include +#include +#include +#include +#include +#include +class DbManager; +class GroupAvatarManager : public QObject { + Q_OBJECT +public: + explicit GroupAvatarManager(DbManager* db, QObject* parent = nullptr); + ~GroupAvatarManager() override; + static GroupAvatarManager* instance(); + QImage image(const QString& channel, quint64 version, const QByteArray& hash); + QImage image(const QString& channel); + QImage currentImage(const QString& channel) const; + void upload(const QString& channel, const QString& source); + void remove(const QString& channel); + void onResult(const uint8_t* bytes, int length); +signals: + void changed(const QString& channel); + void status(const QString& channel, const QString& text); +private: + void publish(const QString& channel, const QByteArray& bytes, quint64 generation); + DbManager* m_db; + QThreadPool m_pool; + QCache m_cache{32 * 1024}; + QHash m_current; + QHash m_generation; + QHash> m_requests; + QSet m_loading, m_invalid; +};