Allow blocking invites from people you don't share a room with
Matrix currently has a significant moderation loophole, thanks to invites. Right now, anyone can invite anyone to a room - and clients like NeoChat will gladly display these rooms to them and even give you a notification. However, this creates a pretty easy attack since room names and avatars are arbitrary and this is a known vector of harassment in the Matrix community. There's currently no tools to block this server-side, so let's try to improve the situation where we can. This adds a new setting to the Security page, wherein it allows you to block invites from people you don't share a room with. This prevents the notification from appearing and NeoChat will attempt to leave the room immediately. Since this depends on MSC 2666 - a currently unstable feature - the server may not support it and NeoChat will disable the setting in this case.
This commit is contained in:
@@ -134,6 +134,8 @@ add_library(neochat STATIC
|
|||||||
jobs/neochatdeletedevicejob.h
|
jobs/neochatdeletedevicejob.h
|
||||||
jobs/neochatchangepasswordjob.cpp
|
jobs/neochatchangepasswordjob.cpp
|
||||||
jobs/neochatchangepasswordjob.h
|
jobs/neochatchangepasswordjob.h
|
||||||
|
jobs/neochatgetcommonroomsjob.cpp
|
||||||
|
jobs/neochatgetcommonroomsjob.h
|
||||||
mediasizehelper.cpp
|
mediasizehelper.cpp
|
||||||
mediasizehelper.h
|
mediasizehelper.h
|
||||||
eventhandler.cpp
|
eventhandler.cpp
|
||||||
|
|||||||
14
src/jobs/neochatgetcommonroomsjob.cpp
Normal file
14
src/jobs/neochatgetcommonroomsjob.cpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "neochatgetcommonroomsjob.h"
|
||||||
|
|
||||||
|
using namespace Quotient;
|
||||||
|
|
||||||
|
NeochatGetCommonRoomsJob::NeochatGetCommonRoomsJob(const QString &userId)
|
||||||
|
: BaseJob(HttpVerb::Get,
|
||||||
|
QStringLiteral("GetCommonRoomsJob"),
|
||||||
|
QStringLiteral("/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms").toLatin1(),
|
||||||
|
QUrlQuery({{QStringLiteral("user_id"), userId}}))
|
||||||
|
{
|
||||||
|
}
|
||||||
14
src/jobs/neochatgetcommonroomsjob.h
Normal file
14
src/jobs/neochatgetcommonroomsjob.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Quotient/jobs/basejob.h>
|
||||||
|
#include <Quotient/omittable.h>
|
||||||
|
|
||||||
|
// TODO: Upstream to libQuotient
|
||||||
|
class NeochatGetCommonRoomsJob : public Quotient::BaseJob
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NeochatGetCommonRoomsJob(const QString &userId);
|
||||||
|
};
|
||||||
@@ -187,5 +187,11 @@
|
|||||||
<default>false</default>
|
<default>false</default>
|
||||||
</entry>
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
|
<group name="Security">
|
||||||
|
<entry name="RejectUnknownInvites" type="bool">
|
||||||
|
<label>Reject unknown invites</label>
|
||||||
|
<default>false</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
</kcfg>
|
</kcfg>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
#include <Quotient/csapi/content-repo.h>
|
#include <Quotient/csapi/content-repo.h>
|
||||||
#include <Quotient/csapi/profile.h>
|
#include <Quotient/csapi/profile.h>
|
||||||
|
#include <Quotient/csapi/versions.h>
|
||||||
#include <Quotient/database.h>
|
#include <Quotient/database.h>
|
||||||
#include <Quotient/jobs/downloadfilejob.h>
|
#include <Quotient/jobs/downloadfilejob.h>
|
||||||
#include <Quotient/qt_connection_util.h>
|
#include <Quotient/qt_connection_util.h>
|
||||||
@@ -132,6 +133,21 @@ void NeoChatConnection::connectSignals()
|
|||||||
Q_EMIT homeNotificationsChanged();
|
Q_EMIT homeNotificationsChanged();
|
||||||
Q_EMIT homeHaveHighlightNotificationsChanged();
|
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"_ls);
|
||||||
|
Q_EMIT canCheckMutualRoomsChanged();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
Qt::SingleShotConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
int NeoChatConnection::badgeNotificationCount() const
|
int NeoChatConnection::badgeNotificationCount() const
|
||||||
@@ -200,6 +216,11 @@ QVariantList NeoChatConnection::getSupportedRoomVersions() const
|
|||||||
return supportedRoomVersions;
|
return supportedRoomVersions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NeoChatConnection::canCheckMutualRooms() const
|
||||||
|
{
|
||||||
|
return m_canCheckMutualRooms;
|
||||||
|
}
|
||||||
|
|
||||||
void NeoChatConnection::changePassword(const QString ¤tPassword, const QString &newPassword)
|
void NeoChatConnection::changePassword(const QString ¤tPassword, const QString &newPassword)
|
||||||
{
|
{
|
||||||
auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
|
auto job = callApi<NeochatChangePasswordJob>(newPassword, false);
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ class NeoChatConnection : public Quotient::Connection
|
|||||||
*/
|
*/
|
||||||
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
|
Q_PROPERTY(bool isOnline READ isOnline WRITE setIsOnline NOTIFY isOnlineChanged)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the server supports querying a user's mutual rooms.
|
||||||
|
*/
|
||||||
|
Q_PROPERTY(bool canCheckMutualRooms READ canCheckMutualRooms NOTIFY canCheckMutualRoomsChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Defines the status after an attempt to change the password on an account.
|
* @brief Defines the status after an attempt to change the password on an account.
|
||||||
@@ -95,6 +100,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void logout(bool serverSideLogout);
|
Q_INVOKABLE void logout(bool serverSideLogout);
|
||||||
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
|
Q_INVOKABLE QVariantList getSupportedRoomVersions() const;
|
||||||
|
bool canCheckMutualRooms() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Change the password for an account.
|
* @brief Change the password for an account.
|
||||||
@@ -196,6 +202,7 @@ Q_SIGNALS:
|
|||||||
void passwordStatus(NeoChatConnection::PasswordStatus status);
|
void passwordStatus(NeoChatConnection::PasswordStatus status);
|
||||||
void userConsentRequired(QUrl url);
|
void userConsentRequired(QUrl url);
|
||||||
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
|
void badgeNotificationCountChanged(NeoChatConnection *connection, int count);
|
||||||
|
void canCheckMutualRoomsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool m_isOnline = true;
|
bool m_isOnline = true;
|
||||||
@@ -208,4 +215,6 @@ private:
|
|||||||
int m_badgeNotificationCount = 0;
|
int m_badgeNotificationCount = 0;
|
||||||
|
|
||||||
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
|
QHash<QUrl, LinkPreviewer *> m_linkPreviewers;
|
||||||
|
|
||||||
|
bool m_canCheckMutualRooms = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "events/joinrulesevent.h"
|
#include "events/joinrulesevent.h"
|
||||||
#include "events/pollevent.h"
|
#include "events/pollevent.h"
|
||||||
#include "filetransferpseudojob.h"
|
#include "filetransferpseudojob.h"
|
||||||
|
#include "jobs/neochatgetcommonroomsjob.h"
|
||||||
#include "neochatconfig.h"
|
#include "neochatconfig.h"
|
||||||
#include "notificationsmanager.h"
|
#include "notificationsmanager.h"
|
||||||
#include "roomlastmessageprovider.h"
|
#include "roomlastmessageprovider.h"
|
||||||
@@ -129,14 +130,38 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
|
auto roomMemberEvent = currentState().get<RoomMemberEvent>(localMember().id());
|
||||||
QImage avatar_image;
|
|
||||||
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
auto showNotification = [this, roomMemberEvent] {
|
||||||
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
|
QImage avatar_image;
|
||||||
|
if (roomMemberEvent && !member(roomMemberEvent->senderId()).avatarUrl().isEmpty()) {
|
||||||
|
avatar_image = memberAvatar(roomMemberEvent->senderId()).get(this->connection(), 128, [] {});
|
||||||
|
} else {
|
||||||
|
qWarning() << "using this room's avatar";
|
||||||
|
avatar_image = avatar(128);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationsManager::instance().postInviteNotification(this,
|
||||||
|
displayName(),
|
||||||
|
member(roomMemberEvent->senderId()).htmlSafeDisplayName(),
|
||||||
|
avatar_image);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (NeoChatConfig::rejectUnknownInvites()) {
|
||||||
|
auto job = this->connection()->callApi<NeochatGetCommonRoomsJob>(roomMemberEvent->senderId());
|
||||||
|
connect(job, &BaseJob::result, this, [this, job, roomMemberEvent, showNotification] {
|
||||||
|
QJsonObject replyData = job->jsonData();
|
||||||
|
if (replyData.contains(QStringLiteral("joined"))) {
|
||||||
|
const bool inAnyOfOurRooms = !replyData[QStringLiteral("joined")].toArray().isEmpty();
|
||||||
|
if (inAnyOfOurRooms) {
|
||||||
|
showNotification();
|
||||||
|
} else {
|
||||||
|
leaveRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "using this room's avatar";
|
showNotification();
|
||||||
avatar_image = avatar(128);
|
|
||||||
}
|
}
|
||||||
NotificationsManager::instance().postInviteNotification(this, displayName(), member(roomMemberEvent->senderId()).htmlSafeDisplayName(), avatar_image);
|
|
||||||
},
|
},
|
||||||
Qt::SingleShotConnection);
|
Qt::SingleShotConnection);
|
||||||
connect(this, &Room::changed, this, [this] {
|
connect(this, &Room::changed, this, [this] {
|
||||||
@@ -1313,7 +1338,6 @@ void NeoChatRoom::setPushNotificationState(PushNotificationState::State state)
|
|||||||
|
|
||||||
m_currentPushNotificationState = state;
|
m_currentPushNotificationState = state;
|
||||||
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
|
Q_EMIT pushNotificationStateChanged(m_currentPushNotificationState);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoChatRoom::updatePushNotificationState(QString type)
|
void NeoChatRoom::updatePushNotificationState(QString type)
|
||||||
|
|||||||
@@ -16,6 +16,32 @@ FormCard.FormCardPage {
|
|||||||
|
|
||||||
title: i18nc("@title", "Security")
|
title: i18nc("@title", "Security")
|
||||||
|
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18nc("@title:group", "Invitations")
|
||||||
|
}
|
||||||
|
FormCard.FormCard {
|
||||||
|
FormCard.FormCheckDelegate {
|
||||||
|
text: i18nc("@option:check", "Reject invitations from unknown users")
|
||||||
|
description: connection.canCheckMutualRooms ? i18n("If enabled, NeoChat will reject invitations from from users you don't share a room with.") : i18n("Your server does not support this setting.")
|
||||||
|
checked: Config.rejectUnknownInvites
|
||||||
|
enabled: !Config.isRejectUnknownInvitesImmutable && connection.canCheckMutualRooms
|
||||||
|
onToggled: {
|
||||||
|
Config.rejectUnknownInvites = checked;
|
||||||
|
Config.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FormCard.FormHeader {
|
||||||
|
title: i18nc("@title:group", "Ignored Users")
|
||||||
|
}
|
||||||
|
FormCard.FormCard {
|
||||||
|
FormCard.FormButtonDelegate {
|
||||||
|
text: i18nc("@action:button", "Manage ignored users")
|
||||||
|
onClicked: root.ApplicationWindow.window.pageStack.push(ignoredUsersDialogComponent, {}, {
|
||||||
|
title: i18nc("@title:window", "Ignored Users")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
FormCard.FormHeader {
|
FormCard.FormHeader {
|
||||||
title: i18nc("@title", "Keys")
|
title: i18nc("@title", "Keys")
|
||||||
}
|
}
|
||||||
@@ -33,17 +59,6 @@ FormCard.FormCardPage {
|
|||||||
description: i18n("Device id")
|
description: i18n("Device id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormCard.FormHeader {
|
|
||||||
title: i18nc("@title:group", "Ignored Users")
|
|
||||||
}
|
|
||||||
FormCard.FormCard {
|
|
||||||
FormCard.FormButtonDelegate {
|
|
||||||
text: i18nc("@action:button", "Manage ignored users")
|
|
||||||
onClicked: pageStack.pushDialogLayer(ignoredUsersDialogComponent, {}, {
|
|
||||||
title: i18nc("@title:window", "Ignored Users")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: ignoredUsersDialogComponent
|
id: ignoredUsersDialogComponent
|
||||||
|
|||||||
Reference in New Issue
Block a user