matrixion/src/SingleImagePackModel.cpp
2023-12-19 16:12:38 +01:00

490 lines
14 KiB
C++

// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "SingleImagePackModel.h"
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <mtx/events/mscs/image_packs.hpp>
#include <nlohmann/json.hpp>
#include <unordered_set>
#include <mtx/responses/media.hpp>
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Utils.h"
#include "timeline/Permissions.h"
#include "timeline/TimelineModel.h"
SingleImagePackModel::SingleImagePackModel(ImagePackInfo pack_, QObject *parent)
: QAbstractListModel(parent)
, roomid_(std::move(pack_.source_room))
, statekey_(std::move(pack_.state_key))
, old_statekey_(statekey_)
, pack(std::move(pack_.pack))
, fromSpace_(pack_.from_space)
{
if (!pack.pack)
pack.pack = mtx::events::msc2545::ImagePack::PackDescription{};
shortcodes.reserve(pack.images.size());
for (const auto &e : pack.images)
shortcodes.push_back(e.first);
connect(this, &SingleImagePackModel::addImage, this, &SingleImagePackModel::addImageCb);
connect(this, &SingleImagePackModel::avatarUploaded, this, &SingleImagePackModel::setAvatarUrl);
}
int
SingleImagePackModel::rowCount(const QModelIndex &) const
{
return (int)shortcodes.size();
}
QHash<int, QByteArray>
SingleImagePackModel::roleNames() const
{
return {
{Roles::Url, "url"},
{Roles::ShortCode, "shortCode"},
{Roles::Body, "body"},
{Roles::IsEmote, "isEmote"},
{Roles::IsSticker, "isSticker"},
};
}
QVariant
SingleImagePackModel::data(const QModelIndex &index, int role) const
{
if (hasIndex(index.row(), index.column(), index.parent())) {
const auto &img = pack.images.at(shortcodes.at(index.row()));
switch (role) {
case Url:
return QString::fromStdString(img.url);
case ShortCode:
return QString::fromStdString(shortcodes.at(index.row()));
case Body:
return QString::fromStdString(img.body);
case IsEmote:
return img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
case IsSticker:
return img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
default:
return {};
}
}
return {};
}
bool
SingleImagePackModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
using mtx::events::msc2545::PackUsage;
if (hasIndex(index.row(), index.column(), index.parent())) {
auto &img = pack.images.at(shortcodes.at(index.row()));
switch (role) {
case ShortCode: {
auto newCode = value.toString().toStdString();
// otherwise we delete this by accident
newCode = unconflictingShortcode(newCode);
auto tmp = img;
auto oldCode = shortcodes.at(index.row());
pack.images.erase(oldCode);
shortcodes[index.row()] = newCode;
pack.images.insert({newCode, tmp});
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::ShortCode});
return true;
}
case Body:
img.body = value.toString().toStdString();
emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::Body});
return true;
case IsEmote: {
bool isEmote = value.toBool();
bool isSticker = img.overrides_usage() ? img.is_sticker() : pack.pack->is_sticker();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(this->index(index.row()), this->index(index.row()), {Roles::IsEmote});
return true;
}
case IsSticker: {
bool isEmote = img.overrides_usage() ? img.is_emoji() : pack.pack->is_emoji();
bool isSticker = value.toBool();
img.usage.set(PackUsage::Emoji, isEmote);
img.usage.set(PackUsage::Sticker, isSticker);
if (img.usage == pack.pack->usage)
img.usage.reset();
emit dataChanged(
this->index(index.row()), this->index(index.row()), {Roles::IsSticker});
return true;
}
}
}
return false;
}
bool
SingleImagePackModel::isGloballyEnabled() const
{
if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
if (auto tmp =
std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
&*roomPacks)) {
if (tmp->content.rooms.count(roomid_) &&
tmp->content.rooms.at(roomid_).count(statekey_))
return true;
}
}
return false;
}
void
SingleImagePackModel::setGloballyEnabled(bool enabled)
{
mtx::events::msc2545::ImagePackRooms content{};
if (auto roomPacks = cache::client()->getAccountData(mtx::events::EventType::ImagePackRooms)) {
if (auto tmp =
std::get_if<mtx::events::EphemeralEvent<mtx::events::msc2545::ImagePackRooms>>(
&*roomPacks)) {
content = tmp->content;
}
}
if (enabled)
content.rooms[roomid_][statekey_] = {};
else
content.rooms[roomid_].erase(statekey_);
http::client()->put_account_data(content, [](mtx::http::RequestErr) {
// emit this->globallyEnabledChanged();
});
}
bool
SingleImagePackModel::canEdit() const
{
if (roomid_.empty())
return true;
else
return Permissions(QString::fromStdString(roomid_))
.canChange(qml_mtx_events::ImagePackInRoom);
}
void
SingleImagePackModel::setPackname(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->display_name) {
this->pack.pack->display_name = val_;
emit packnameChanged();
}
}
void
SingleImagePackModel::setAttribution(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->attribution) {
this->pack.pack->attribution = val_;
emit attributionChanged();
}
}
void
SingleImagePackModel::setAvatarUrl(QString val)
{
auto val_ = val.toStdString();
if (val_ != this->pack.pack->avatar_url) {
this->pack.pack->avatar_url = val_;
emit avatarUrlChanged();
}
}
QString
SingleImagePackModel::avatarUrl() const
{
if (!pack.pack->avatar_url.empty())
return QString::fromStdString(pack.pack->avatar_url);
else if (!pack.images.empty())
return QString::fromStdString(pack.images.begin()->second.url);
else
return QString();
}
void
SingleImagePackModel::setStatekey(QString val)
{
auto val_ = val.toStdString();
if (val_ != statekey_) {
statekey_ = val_;
// prevent deleting current pack
if (!roomid_.empty() && statekey_ != old_statekey_) {
statekey_ = unconflictingStatekey(roomid_, statekey_);
}
emit statekeyChanged();
}
}
void
SingleImagePackModel::setIsStickerPack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_sticker()) {
pack.pack->usage.set(PackUsage::Sticker, val);
if (!val)
pack.pack->usage.set(PackUsage::Emoji, true);
emit isEmotePackChanged();
emit isStickerPackChanged();
}
}
void
SingleImagePackModel::setIsEmotePack(bool val)
{
using mtx::events::msc2545::PackUsage;
if (val != pack.pack->is_emoji()) {
pack.pack->usage.set(PackUsage::Emoji, val);
if (!val)
pack.pack->usage.set(PackUsage::Sticker, true);
emit isEmotePackChanged();
emit isStickerPackChanged();
}
}
void
SingleImagePackModel::save()
{
if (roomid_.empty()) {
http::client()->put_account_data(pack, [](mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
});
} else {
if (old_statekey_ != statekey_) {
this->remove();
}
http::client()->send_state_event(
roomid_,
statekey_,
pack,
[this](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
nhlog::net()->info("Uploaded image pack: %1", statekey_);
});
}
}
void
SingleImagePackModel::remove()
{
// handle account pack deletion.
// Sadly we cannot actually delete the pack,
// so we just send an empty pack to clear out its information.
if (roomid_.empty()) {
http::client()->put_account_data(
mtx::events::msc2545::ImagePack(), [](mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to update image pack: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
});
return;
}
http::client()->send_state_event(
roomid_,
to_string(mtx::events::EventType::ImagePackInRoom),
old_statekey_,
nlohmann::json::object(),
[](const mtx::responses::EventId &, mtx::http::RequestErr e) {
if (e)
ChatPage::instance()->showNotification(
tr("Failed to delete old image pack: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
});
old_statekey_ = statekey_;
}
void
SingleImagePackModel::addStickers(QList<QUrl> files)
{
for (const auto &f : files) {
auto file = QFile(f.toLocalFile());
if (!file.open(QFile::ReadOnly)) {
ChatPage::instance()->showNotification(
tr("Failed to open image: %1").arg(f.toLocalFile()));
return;
}
auto bytes = file.readAll();
auto img = utils::readImage(bytes);
mtx::common::ImageInfo info{};
auto sz = img.size() / 2;
if (sz.width() > 512 || sz.height() > 512) {
sz.scale(512, 512, Qt::AspectRatioMode::KeepAspectRatio);
} else if (img.height() < 128 && img.width() < 128) {
sz = img.size();
}
info.h = sz.height();
info.w = sz.width();
info.size = bytes.size();
info.mimetype = QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString();
auto filename = f.fileName().toStdString();
auto basename = QFileInfo(file).baseName().toStdString();
http::client()->upload(
bytes.toStdString(),
QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
filename,
[this, basename, info](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) {
if (e) {
ChatPage::instance()->showNotification(
tr("Failed to upload image: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
return;
}
emit addImage(uri.content_uri, basename, info);
});
}
}
void
SingleImagePackModel::setAvatar(QUrl f)
{
auto file = QFile(f.toLocalFile());
if (!file.open(QFile::ReadOnly)) {
ChatPage::instance()->showNotification(tr("Failed to open image: %1").arg(f.toLocalFile()));
return;
}
auto bytes = file.readAll();
auto filename = f.fileName().toStdString();
http::client()->upload(
bytes.toStdString(),
QMimeDatabase().mimeTypeForFile(f.toLocalFile()).name().toStdString(),
filename,
[this, filename](const mtx::responses::ContentURI &uri, mtx::http::RequestErr e) {
if (e) {
ChatPage::instance()->showNotification(
tr("Failed to upload image: %1")
.arg(QString::fromStdString(e->matrix_error.error)));
return;
}
emit avatarUploaded(QString::fromStdString(uri.content_uri));
});
}
void
SingleImagePackModel::remove(int idx)
{
if (idx < (int)shortcodes.size() && idx >= 0) {
beginRemoveRows(QModelIndex(), idx, idx);
auto s = shortcodes.at(idx);
shortcodes.erase(shortcodes.begin() + idx);
pack.images.erase(s);
endRemoveRows();
}
}
std::string
SingleImagePackModel::unconflictingShortcode(const std::string &shortcode)
{
if (pack.images.count(shortcode)) {
// more images won't fit in an event anyway
for (int i = 0; i < 64'000; i++) {
auto tempCode = shortcode + std::to_string(i);
if (!pack.images.count(tempCode)) {
return tempCode;
}
}
}
return shortcode;
}
std::string
SingleImagePackModel::unconflictingStatekey(const std::string &roomid, const std::string &key)
{
if (roomid.empty())
return key;
std::unordered_set<std::string> statekeys;
auto currentPacks =
cache::client()->getStateEventsWithType<mtx::events::msc2545::ImagePack>(roomid);
for (const auto &pack : currentPacks) {
if (!pack.content.images.empty())
statekeys.insert(pack.state_key);
}
auto defaultPack = cache::client()->getStateEvent<mtx::events::msc2545::ImagePack>(roomid);
if (defaultPack && defaultPack->content.images.size()) {
statekeys.insert(defaultPack->state_key);
}
if (statekeys.count(key)) {
// arbitrary count. More than 64k image packs in a room are unlikely and if you have that,
// you probably know what you are doing :)
for (int i = 0; i < 64'000; i++) {
auto tempCode = key + std::to_string(i);
if (!statekeys.count(tempCode)) {
return tempCode;
}
}
}
return key;
}
void
SingleImagePackModel::addImageCb(std::string uri, std::string filename, mtx::common::ImageInfo info)
{
mtx::events::msc2545::PackImage img{};
img.url = uri;
img.info = info;
beginInsertRows(
QModelIndex(), static_cast<int>(shortcodes.size()), static_cast<int>(shortcodes.size()));
auto shortcode = unconflictingShortcode(filename);
pack.images[shortcode] = img;
shortcodes.push_back(shortcode);
endInsertRows();
if (this->pack.pack->avatar_url.empty())
this->setAvatarUrl(QString::fromStdString(uri));
}