Currently the way we show invite notifications is sub-optimal. We did it during the initial room state load, which meant it shows an invite notification *every time* you opened NeoChat. This gets annoying very quickly if you have any pending invitations you don't want to take action on just yet. Instead, let's handle this in NotificationsManager directly, and also remove some scaffolding now that it isn't plumbed through NeoChatRoom/NeoChatConnection.
540 lines
18 KiB
C++
540 lines
18 KiB
C++
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "neochatconnection.h"
|
|
|
|
#include <QImageReader>
|
|
#include <QJsonDocument>
|
|
|
|
#include "neochatconfig.h"
|
|
#include "neochatroom.h"
|
|
#include "spacehierarchycache.h"
|
|
|
|
#include <Quotient/jobs/basejob.h>
|
|
#include <Quotient/quotient_common.h>
|
|
#include <qt6keychain/keychain.h>
|
|
|
|
#include <KLocalizedString>
|
|
|
|
#include <Quotient/csapi/content-repo.h>
|
|
#include <Quotient/csapi/profile.h>
|
|
#include <Quotient/csapi/registration.h>
|
|
#include <Quotient/csapi/versions.h>
|
|
#include <Quotient/jobs/downloadfilejob.h>
|
|
#include <Quotient/qt_connection_util.h>
|
|
#include <Quotient/room.h>
|
|
#include <Quotient/settings.h>
|
|
#include <Quotient/user.h>
|
|
|
|
#ifdef HAVE_KUNIFIEDPUSH
|
|
#include <QCoroNetwork>
|
|
#include <Quotient/csapi/pusher.h>
|
|
#include <Quotient/networkaccessmanager.h>
|
|
#endif
|
|
|
|
using namespace Quotient;
|
|
using namespace Qt::StringLiterals;
|
|
|
|
NeoChatConnection::NeoChatConnection(QObject *parent)
|
|
: Connection(parent)
|
|
, m_threePIdModel(new ThreePIdModel(this))
|
|
{
|
|
m_linkPreviewers.setMaxCost(20);
|
|
connectSignals();
|
|
}
|
|
|
|
NeoChatConnection::NeoChatConnection(const QUrl &server, QObject *parent)
|
|
: Connection(server, parent)
|
|
, m_threePIdModel(new ThreePIdModel(this))
|
|
{
|
|
m_linkPreviewers.setMaxCost(20);
|
|
connectSignals();
|
|
}
|
|
|
|
void NeoChatConnection::connectSignals()
|
|
{
|
|
connect(this, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) {
|
|
if (type == u"org.kde.neochat.account_label"_s) {
|
|
Q_EMIT labelChanged();
|
|
}
|
|
if (type == u"m.identity_server"_s) {
|
|
Q_EMIT identityServerChanged();
|
|
}
|
|
});
|
|
connect(this, &NeoChatConnection::syncDone, this, [this] {
|
|
setIsOnline(true);
|
|
});
|
|
connect(this, &NeoChatConnection::networkError, this, [this]() {
|
|
setIsOnline(false);
|
|
});
|
|
connect(this, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
|
|
if (job->error() == BaseJob::UserConsentRequired) {
|
|
Q_EMIT userConsentRequired(job->errorUrl());
|
|
}
|
|
});
|
|
connect(this, &NeoChatConnection::requestFailed, this, [this](BaseJob *job) {
|
|
if (dynamic_cast<DownloadFileJob *>(job) && job->jsonData()["errcode"_L1].toString() == "M_TOO_LARGE"_L1) {
|
|
Q_EMIT showMessage(MessageType::Warning, i18n("File too large to download.<br />Contact your matrix server administrator for support."));
|
|
}
|
|
});
|
|
connect(this, &NeoChatConnection::directChatsListChanged, this, [this](DirectChatsMap additions, DirectChatsMap removals) {
|
|
Q_EMIT directChatInvitesChanged();
|
|
for (const auto &chatId : additions) {
|
|
if (const auto chat = room(chatId)) {
|
|
connect(chat, &Room::unreadStatsChanged, this, [this]() {
|
|
refreshBadgeNotificationCount();
|
|
Q_EMIT directChatNotificationsChanged();
|
|
Q_EMIT directChatsHaveHighlightNotificationsChanged();
|
|
});
|
|
}
|
|
}
|
|
for (const auto &chatId : removals) {
|
|
if (const auto chat = room(chatId)) {
|
|
disconnect(chat, &Room::unreadStatsChanged, this, nullptr);
|
|
}
|
|
}
|
|
});
|
|
connect(this, &NeoChatConnection::joinedRoom, this, [this](Room *room) {
|
|
if (room->isDirectChat()) {
|
|
connect(room, &Room::unreadStatsChanged, this, [this]() {
|
|
Q_EMIT directChatNotificationsChanged();
|
|
Q_EMIT directChatsHaveHighlightNotificationsChanged();
|
|
});
|
|
}
|
|
connect(room, &Room::unreadStatsChanged, this, [this]() {
|
|
refreshBadgeNotificationCount();
|
|
Q_EMIT homeNotificationsChanged();
|
|
Q_EMIT homeHaveHighlightNotificationsChanged();
|
|
});
|
|
});
|
|
connect(this, &NeoChatConnection::leftRoom, this, [this](Room *room, Room *prev) {
|
|
Q_UNUSED(room)
|
|
if (prev && prev->isDirectChat()) {
|
|
Q_EMIT directChatInvitesChanged();
|
|
Q_EMIT directChatNotificationsChanged();
|
|
Q_EMIT directChatsHaveHighlightNotificationsChanged();
|
|
}
|
|
refreshBadgeNotificationCount();
|
|
Q_EMIT homeNotificationsChanged();
|
|
Q_EMIT homeHaveHighlightNotificationsChanged();
|
|
});
|
|
|
|
connect(&SpaceHierarchyCache::instance(), &SpaceHierarchyCache::spaceHierarchyChanged, this, [this]() {
|
|
refreshBadgeNotificationCount();
|
|
Q_EMIT homeNotificationsChanged();
|
|
Q_EMIT homeHaveHighlightNotificationsChanged();
|
|
});
|
|
|
|
// Fetch unstable features
|
|
// TODO: Expose unstableFeatures() in libQuotient
|
|
connect(
|
|
this,
|
|
&Connection::connected,
|
|
this,
|
|
[this] {
|
|
auto job = callApi<GetVersionsJob>(BackgroundRequest);
|
|
connect(job, &GetVersionsJob::success, this, [this, job] {
|
|
m_canCheckMutualRooms = job->unstableFeatures().contains("uk.half-shot.msc2666.query_mutual_rooms"_L1);
|
|
Q_EMIT canCheckMutualRoomsChanged();
|
|
m_canEraseData = job->unstableFeatures().contains("org.matrix.msc4025"_L1) || job->versions().count("v1.10"_L1);
|
|
Q_EMIT canEraseDataChanged();
|
|
});
|
|
},
|
|
Qt::SingleShotConnection);
|
|
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
|
|
connect(NeoChatConfig::self(), &NeoChatConfig::PreferUsingEncryptionChanged, this, [] {
|
|
setDirectChatEncryptionDefault(NeoChatConfig::preferUsingEncryption());
|
|
});
|
|
}
|
|
|
|
int NeoChatConnection::badgeNotificationCount() const
|
|
{
|
|
return m_badgeNotificationCount;
|
|
}
|
|
|
|
void NeoChatConnection::refreshBadgeNotificationCount()
|
|
{
|
|
int count = 0;
|
|
for (const auto &r : allRooms()) {
|
|
if (const auto room = static_cast<NeoChatRoom *>(r)) {
|
|
count += room->contextAwareNotificationCount();
|
|
}
|
|
}
|
|
|
|
if (count != m_badgeNotificationCount) {
|
|
m_badgeNotificationCount = count;
|
|
Q_EMIT badgeNotificationCountChanged(this, m_badgeNotificationCount);
|
|
}
|
|
}
|
|
|
|
void NeoChatConnection::logout(bool serverSideLogout)
|
|
{
|
|
SettingsGroup(u"Accounts"_s).remove(userId());
|
|
|
|
QKeychain::DeletePasswordJob job(qAppName());
|
|
job.setAutoDelete(true);
|
|
job.setKey(userId());
|
|
QEventLoop loop;
|
|
QKeychain::DeletePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
|
job.start();
|
|
loop.exec();
|
|
|
|
if (!serverSideLogout) {
|
|
return;
|
|
}
|
|
Connection::logout();
|
|
}
|
|
|
|
bool NeoChatConnection::setAvatar(const QUrl &avatarSource)
|
|
{
|
|
QString decoded = avatarSource.path();
|
|
if (decoded.isEmpty()) {
|
|
callApi<SetAvatarUrlJob>(user()->id(), avatarSource);
|
|
return true;
|
|
}
|
|
if (QImageReader(decoded).read().isNull()) {
|
|
return false;
|
|
} else {
|
|
return user()->setAvatar(decoded);
|
|
}
|
|
}
|
|
|
|
QVariantList NeoChatConnection::getSupportedRoomVersions() const
|
|
{
|
|
const auto &roomVersions = availableRoomVersions();
|
|
QVariantList supportedRoomVersions;
|
|
for (const auto &v : roomVersions) {
|
|
QVariantMap roomVersionMap;
|
|
roomVersionMap.insert("id"_L1, v.id);
|
|
roomVersionMap.insert("status"_L1, v.status);
|
|
roomVersionMap.insert("isStable"_L1, v.isStable());
|
|
supportedRoomVersions.append(roomVersionMap);
|
|
}
|
|
return supportedRoomVersions;
|
|
}
|
|
|
|
bool NeoChatConnection::canCheckMutualRooms() const
|
|
{
|
|
return m_canCheckMutualRooms;
|
|
}
|
|
|
|
void NeoChatConnection::changePassword(const QString ¤tPassword, const QString &newPassword)
|
|
{
|
|
auto job = callApi<ChangePasswordJob>(newPassword, false);
|
|
connect(job, &BaseJob::result, this, [this, job, currentPassword, newPassword] {
|
|
if (job->error() == 103) {
|
|
QJsonObject replyData = job->jsonData();
|
|
AuthenticationData authData;
|
|
authData.session = replyData["session"_L1].toString();
|
|
authData.type = "m.login.password"_L1;
|
|
authData.authInfo["password"_L1] = currentPassword;
|
|
authData.authInfo["user"_L1] = user()->id();
|
|
authData.authInfo["identifier"_L1] = QJsonObject{{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
|
|
auto innerJob = callApi<ChangePasswordJob>(newPassword, false, authData);
|
|
connect(innerJob, &BaseJob::success, this, [this]() {
|
|
Q_EMIT passwordStatus(PasswordStatus::Success);
|
|
});
|
|
connect(innerJob, &BaseJob::failure, this, [innerJob, this]() {
|
|
Q_EMIT passwordStatus(innerJob->jsonData()["errcode"_L1] == "M_FORBIDDEN"_L1 ? PasswordStatus::Wrong : PasswordStatus::Other);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void NeoChatConnection::setLabel(const QString &label)
|
|
{
|
|
QJsonObject json{
|
|
{"account_label"_L1, label},
|
|
};
|
|
setAccountData("org.kde.neochat.account_label"_L1, json);
|
|
Q_EMIT labelChanged();
|
|
}
|
|
|
|
QString NeoChatConnection::label() const
|
|
{
|
|
return accountDataJson("org.kde.neochat.account_label"_L1)["account_label"_L1].toString();
|
|
}
|
|
|
|
void NeoChatConnection::deactivateAccount(const QString &password, const bool erase)
|
|
{
|
|
auto job = callApi<DeactivateAccountJob>();
|
|
connect(job, &BaseJob::result, this, [this, job, password, erase] {
|
|
if (job->error() == 103) {
|
|
QJsonObject replyData = job->jsonData();
|
|
AuthenticationData authData;
|
|
authData.session = replyData["session"_L1].toString();
|
|
authData.authInfo["password"_L1] = password;
|
|
authData.type = "m.login.password"_L1;
|
|
authData.authInfo["user"_L1] = user()->id();
|
|
QJsonObject identifier = {{"type"_L1, "m.id.user"_L1}, {"user"_L1, user()->id()}};
|
|
authData.authInfo["identifier"_L1] = identifier;
|
|
auto innerJob = callApi<DeactivateAccountJob>(authData, QString{}, erase);
|
|
connect(innerJob, &BaseJob::success, this, [this]() {
|
|
logout(false);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
ThreePIdModel *NeoChatConnection::threePIdModel() const
|
|
{
|
|
return m_threePIdModel;
|
|
}
|
|
|
|
bool NeoChatConnection::hasIdentityServer() const
|
|
{
|
|
if (!hasAccountData(u"m.identity_server"_s)) {
|
|
return false;
|
|
}
|
|
|
|
const auto url = accountData(u"m.identity_server"_s)->contentPart<QUrl>("base_url"_L1);
|
|
if (!url.isEmpty()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QUrl NeoChatConnection::identityServer() const
|
|
{
|
|
if (!hasAccountData(u"m.identity_server"_s)) {
|
|
return {};
|
|
}
|
|
|
|
const auto url = accountData(u"m.identity_server"_s)->contentPart<QUrl>("base_url"_L1);
|
|
if (!url.isEmpty()) {
|
|
return url;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
QString NeoChatConnection::identityServerUIString() const
|
|
{
|
|
if (!hasIdentityServer()) {
|
|
return i18nc("@info", "No identity server configured");
|
|
}
|
|
|
|
return identityServer().toString();
|
|
}
|
|
|
|
void NeoChatConnection::createRoom(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
|
|
{
|
|
QList<CreateRoomJob::StateEvent> initialStateEvents;
|
|
if (!parent.isEmpty()) {
|
|
initialStateEvents.append(CreateRoomJob::StateEvent{
|
|
"m.space.parent"_L1,
|
|
QJsonObject{
|
|
{"canonical"_L1, true},
|
|
{"via"_L1, QJsonArray{domain()}},
|
|
},
|
|
parent,
|
|
});
|
|
}
|
|
|
|
const auto job = Connection::createRoom(Connection::PublishRoom, QString(), name, topic, QStringList(), {}, {}, {}, initialStateEvents);
|
|
if (!parent.isEmpty()) {
|
|
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
|
|
if (setChildParent) {
|
|
if (auto parentRoom = room(parent)) {
|
|
parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
|
Q_EMIT errorOccured(i18n("Room creation failed: %1", job->errorString()));
|
|
});
|
|
}
|
|
|
|
void NeoChatConnection::createSpace(const QString &name, const QString &topic, const QString &parent, bool setChildParent)
|
|
{
|
|
QList<CreateRoomJob::StateEvent> initialStateEvents;
|
|
if (!parent.isEmpty()) {
|
|
initialStateEvents.append(CreateRoomJob::StateEvent{
|
|
"m.space.parent"_L1,
|
|
QJsonObject{
|
|
{"canonical"_L1, true},
|
|
{"via"_L1, QJsonArray{domain()}},
|
|
},
|
|
parent,
|
|
});
|
|
}
|
|
|
|
const auto job =
|
|
Connection::createRoom(Connection::UnpublishRoom, {}, name, topic, {}, {}, {}, false, initialStateEvents, {}, QJsonObject{{"type"_L1, "m.space"_L1}});
|
|
if (!parent.isEmpty()) {
|
|
connect(job, &Quotient::CreateRoomJob::success, this, [this, parent, setChildParent, job]() {
|
|
if (setChildParent) {
|
|
if (auto parentRoom = room(parent)) {
|
|
parentRoom->setState(u"m.space.child"_s, job->roomId(), QJsonObject{{"via"_L1, QJsonArray{domain()}}});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
connect(job, &CreateRoomJob::failure, this, [this, job] {
|
|
Q_EMIT errorOccured(i18n("Space creation failed: %1", job->errorString()));
|
|
});
|
|
}
|
|
|
|
bool NeoChatConnection::directChatExists(Quotient::User *user)
|
|
{
|
|
return directChats().contains(user);
|
|
}
|
|
|
|
qsizetype NeoChatConnection::directChatNotifications() const
|
|
{
|
|
qsizetype notifications = 0;
|
|
QStringList added; // The same ID can be in the list multiple times.
|
|
for (const auto &chatId : directChats()) {
|
|
if (!added.contains(chatId)) {
|
|
if (const auto chat = room(chatId)) {
|
|
notifications += dynamic_cast<NeoChatRoom *>(chat)->contextAwareNotificationCount();
|
|
added += chatId;
|
|
}
|
|
}
|
|
}
|
|
return notifications;
|
|
}
|
|
|
|
bool NeoChatConnection::directChatsHaveHighlightNotifications() const
|
|
{
|
|
for (const auto &childId : directChats()) {
|
|
if (const auto child = static_cast<NeoChatRoom *>(room(childId))) {
|
|
if (child->highlightCount() > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
qsizetype NeoChatConnection::homeNotifications() const
|
|
{
|
|
qsizetype notifications = 0;
|
|
QStringList added;
|
|
const auto &spaceHierarchyCache = SpaceHierarchyCache::instance();
|
|
for (const auto &r : allRooms()) {
|
|
if (const auto room = static_cast<NeoChatRoom *>(r)) {
|
|
if (!added.contains(room->id()) && !room->isDirectChat() && !spaceHierarchyCache.isChild(room->id())) {
|
|
notifications += dynamic_cast<NeoChatRoom *>(room)->contextAwareNotificationCount();
|
|
added += room->id();
|
|
}
|
|
}
|
|
}
|
|
return notifications;
|
|
}
|
|
|
|
bool NeoChatConnection::homeHaveHighlightNotifications() const
|
|
{
|
|
const auto &spaceHierarchyCache = SpaceHierarchyCache::instance();
|
|
for (const auto &r : allRooms()) {
|
|
if (const auto room = static_cast<NeoChatRoom *>(r)) {
|
|
if (!room->isDirectChat() && !spaceHierarchyCache.isChild(room->id()) && room->highlightCount() > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool NeoChatConnection::directChatInvites() const
|
|
{
|
|
auto inviteRooms = rooms(JoinState::Invite);
|
|
for (const auto inviteRoom : inviteRooms) {
|
|
if (inviteRoom->isDirectChat()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QCoro::Task<void> NeoChatConnection::setupPushNotifications(QString endpoint)
|
|
{
|
|
#ifdef HAVE_KUNIFIEDPUSH
|
|
QUrl gatewayEndpoint(endpoint);
|
|
gatewayEndpoint.setPath(u"/_matrix/push/v1/notify"_s);
|
|
|
|
QNetworkRequest checkGateway(gatewayEndpoint);
|
|
auto reply = co_await NetworkAccessManager::instance()->get(checkGateway);
|
|
|
|
// We want to check if this UnifiedPush server has a Matrix gateway
|
|
// This is because Matrix does not natively support UnifiedPush
|
|
const auto &replyJson = QJsonDocument::fromJson(reply->readAll()).object();
|
|
|
|
if (replyJson["unifiedpush"_L1]["gateway"_L1].toString() == u"matrix"_s) {
|
|
callApi<PostPusherJob>(endpoint,
|
|
u"http"_s,
|
|
u"org.kde.neochat"_s,
|
|
u"NeoChat"_s,
|
|
deviceId(),
|
|
QString(), // profileTag is intentionally left empty for now, it's optional
|
|
u"en-US"_s,
|
|
PostPusherJob::PusherData{QUrl::fromUserInput(gatewayEndpoint.toString()), u" "_s},
|
|
false);
|
|
|
|
qInfo() << "Registered for push notifications";
|
|
} else {
|
|
qWarning() << "There's no gateway, not setting up push notifications.";
|
|
}
|
|
#else
|
|
Q_UNUSED(endpoint)
|
|
co_return;
|
|
#endif
|
|
}
|
|
|
|
bool NeoChatConnection::isOnline() const
|
|
{
|
|
return m_isOnline;
|
|
}
|
|
|
|
void NeoChatConnection::setIsOnline(bool isOnline)
|
|
{
|
|
if (isOnline == m_isOnline) {
|
|
return;
|
|
}
|
|
m_isOnline = isOnline;
|
|
Q_EMIT isOnlineChanged();
|
|
}
|
|
|
|
QString NeoChatConnection::accountDataJsonString(const QString &type) const
|
|
{
|
|
return QString::fromUtf8(QJsonDocument(accountDataJson(type)).toJson());
|
|
}
|
|
|
|
LinkPreviewer *NeoChatConnection::previewerForLink(const QUrl &link)
|
|
{
|
|
if (!NeoChatConfig::showLinkPreview()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto previewer = m_linkPreviewers.object(link);
|
|
if (previewer != nullptr) {
|
|
return previewer;
|
|
}
|
|
|
|
previewer = new LinkPreviewer(link, this);
|
|
m_linkPreviewers.insert(link, previewer);
|
|
return previewer;
|
|
}
|
|
|
|
KeyImport::Error NeoChatConnection::exportMegolmSessions(const QString &passphrase, const QString &path)
|
|
{
|
|
KeyImport keyImport;
|
|
auto result = keyImport.exportKeys(passphrase, this);
|
|
if (!result.has_value()) {
|
|
return result.error();
|
|
}
|
|
QUrl url(path);
|
|
QFile file(url.toLocalFile());
|
|
file.open(QFile::WriteOnly);
|
|
file.write(result.value());
|
|
file.close();
|
|
return KeyImport::Success;
|
|
}
|
|
|
|
bool NeoChatConnection::canEraseData() const
|
|
{
|
|
return m_canEraseData;
|
|
}
|
|
|
|
#include "moc_neochatconnection.cpp"
|