Invite permission management using the /allowinvites and /blockinvites commands
Rejecting such invites should be handled serverside in the future and there should be a GUI for this, but for now this should work.
This commit is contained in:
parent
5ef67d2a91
commit
978174af77
10 changed files with 266 additions and 101 deletions
|
|
@ -615,7 +615,7 @@ if(USE_BUNDLED_MTXCLIENT)
|
|||
FetchContent_Declare(
|
||||
MatrixClient
|
||||
GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
|
||||
GIT_TAG a0b203980491ddf2e2fe4f1cd6af8c2562b3ee35
|
||||
GIT_TAG d6a0a4ebee83275dbbeb999679a22a7d238326ff
|
||||
)
|
||||
set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
|
||||
set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ modules:
|
|||
- -DBUILD_SHARED_LIBS=OFF
|
||||
buildsystem: cmake-ninja
|
||||
sources:
|
||||
- commit: a0b203980491ddf2e2fe4f1cd6af8c2562b3ee35
|
||||
- commit: d6a0a4ebee83275dbbeb999679a22a7d238326ff
|
||||
#tag: v0.10.0
|
||||
type: git
|
||||
url: https://github.com/Nheko-Reborn/mtxclient.git
|
||||
|
|
|
|||
|
|
@ -243,6 +243,14 @@ Ignore a user, invites from them are also rejected.
|
|||
*/unignore* _<username>_::
|
||||
Stops ignoring a user.
|
||||
|
||||
=== Invite permission management
|
||||
|
||||
*/blockinvites* _<username>_|_<roomid>_|_<servername>_|all::
|
||||
Block all invites either by default ("all") or from a specific user or server or to a specific room.
|
||||
|
||||
*/allowinvites* _<username>_|_<roomid>_|_<servername>_|all::
|
||||
Allow all invites either by default ("all") or from a specific user or server or to a specific room.
|
||||
|
||||
=== Advanced
|
||||
|
||||
*/clear-timeline*::
|
||||
|
|
|
|||
133
src/ChatPage.cpp
133
src/ChatPage.cpp
|
|
@ -807,6 +807,36 @@ ChatPage::handleSyncResponse(const mtx::responses::Sync &res, const std::string
|
|||
cache::client()->saveState(res);
|
||||
olm::handle_to_device_messages(res.to_device.events);
|
||||
|
||||
// reject forbidden invites
|
||||
if (!res.rooms.invite.empty()) {
|
||||
if (auto ev =
|
||||
cache::client()->getAccountData(mtx::events::EventType::NhekoInvitePermissions)) {
|
||||
const auto &invitePerms = std::get<mtx::events::AccountDataEvent<
|
||||
mtx::events::account_data::nheko_extensions::InvitePermissions>>(*ev)
|
||||
.content;
|
||||
|
||||
for (const auto &[roomid, invite] : res.rooms.invite) {
|
||||
std::string_view inviter = "";
|
||||
for (const auto &memberEv : invite.invite_state) {
|
||||
if (auto member =
|
||||
std::get_if<mtx::events::StrippedEvent<mtx::events::state::Member>>(
|
||||
&memberEv)) {
|
||||
if (member->content.membership ==
|
||||
mtx::events::state::Membership::Invite &&
|
||||
member->state_key == http::client()->user_id().to_string()) {
|
||||
inviter = member->sender;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!invitePerms.invite_allowed(roomid, inviter)) {
|
||||
leaveRoom(QString::fromStdString(roomid), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit syncUI(std::move(res));
|
||||
|
||||
// if the ignored users changed, clear timeline of all affected rooms.
|
||||
|
|
@ -1583,112 +1613,19 @@ ChatPage::startChat(QString userid, std::optional<bool> encryptionEnabled)
|
|||
emit ChatPage::instance()->createRoom(req);
|
||||
}
|
||||
|
||||
static QString
|
||||
mxidFromSegments(QStringView sigil, QStringView mxid)
|
||||
{
|
||||
if (mxid.isEmpty())
|
||||
return QString();
|
||||
|
||||
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
|
||||
|
||||
if (sigil == u"u") {
|
||||
return "@" + mxid_;
|
||||
} else if (sigil == u"roomid") {
|
||||
return "!" + mxid_;
|
||||
} else if (sigil == u"r") {
|
||||
return "#" + mxid_;
|
||||
//} else if (sigil == "group") {
|
||||
// return "+" + mxid_;
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ChatPage::handleMatrixUri(QString uri)
|
||||
{
|
||||
nhlog::ui()->info("Received uri! {}", uri.toStdString());
|
||||
QUrl uri_{uri};
|
||||
|
||||
// Convert matrix.to URIs to proper format
|
||||
if (uri_.scheme() == QLatin1String("https") && uri_.host() == QLatin1String("matrix.to")) {
|
||||
QString p = uri_.fragment(QUrl::FullyEncoded);
|
||||
if (p.startsWith(QLatin1String("/")))
|
||||
p.remove(0, 1);
|
||||
auto m = utils::parseMatrixUri(uri);
|
||||
|
||||
auto temp = p.split(QStringLiteral("?"));
|
||||
QString query;
|
||||
if (temp.size() >= 2)
|
||||
query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
|
||||
|
||||
temp = temp.first().split(QStringLiteral("/"));
|
||||
auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
|
||||
QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
|
||||
if (!identifier.isEmpty()) {
|
||||
if (identifier.startsWith(QLatin1String("@"))) {
|
||||
QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
return handleMatrixUri(QUrl::fromEncoded(newUri));
|
||||
} else if (identifier.startsWith(QLatin1String("#"))) {
|
||||
QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!eventId.isEmpty())
|
||||
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
return handleMatrixUri(QUrl::fromEncoded(newUri));
|
||||
} else if (identifier.startsWith(QLatin1String("!"))) {
|
||||
QByteArray newUri =
|
||||
"matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!eventId.isEmpty())
|
||||
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
return handleMatrixUri(QUrl::fromEncoded(newUri));
|
||||
}
|
||||
}
|
||||
if (!m) {
|
||||
nhlog::ui()->info("failed to parse uri! {}", uri.toStdString());
|
||||
return false;
|
||||
}
|
||||
|
||||
// non-matrix URIs are not handled by us, return false
|
||||
if (uri_.scheme() != QLatin1String("matrix"))
|
||||
return false;
|
||||
|
||||
auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
|
||||
if (tempPath.startsWith('/'))
|
||||
tempPath.remove(0, 1);
|
||||
auto segments = QStringView(tempPath).split('/');
|
||||
|
||||
if (segments.size() != 2 && segments.size() != 4)
|
||||
return false;
|
||||
|
||||
auto sigil1 = segments[0];
|
||||
auto mxid1 = mxidFromSegments(sigil1, segments[1]);
|
||||
if (mxid1.isEmpty())
|
||||
return false;
|
||||
|
||||
QString mxid2;
|
||||
if (segments.size() == 4 && segments[2] == QStringView(u"e")) {
|
||||
if (segments[3].isEmpty())
|
||||
return false;
|
||||
else
|
||||
mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
|
||||
}
|
||||
|
||||
std::vector<std::string> vias;
|
||||
QString action;
|
||||
|
||||
auto items =
|
||||
uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&', Qt::SkipEmptyParts);
|
||||
for (QString item : std::as_const(items)) {
|
||||
nhlog::ui()->info("item: {}", item.toStdString());
|
||||
|
||||
if (item.startsWith(QLatin1String("action="))) {
|
||||
action = item.remove(QStringLiteral("action="));
|
||||
} else if (item.startsWith(QLatin1String("via="))) {
|
||||
vias.push_back(QUrl::fromPercentEncoding(item.remove(QStringLiteral("via=")).toUtf8())
|
||||
.toStdString());
|
||||
}
|
||||
}
|
||||
const auto &[sigil1, mxid1, sigil2, mxid2, action, vias] = *m;
|
||||
|
||||
if (sigil1 == u"u") {
|
||||
if (action.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
|
|||
return QStringLiteral("/ignore");
|
||||
case Unignore:
|
||||
return QStringLiteral("/unignore");
|
||||
case BlockInvites:
|
||||
return QStringLiteral("/blockinvites");
|
||||
case AllowInvites:
|
||||
return QStringLiteral("/allowinvites");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
|
@ -186,6 +190,10 @@ CommandCompleter::data(const QModelIndex &index, int role) const
|
|||
return QStringLiteral("/ignore <@userid>");
|
||||
case Unignore:
|
||||
return QStringLiteral("/unignore <@userid>");
|
||||
case BlockInvites:
|
||||
return QStringLiteral("/blockinvites <@userid>|<!roomid>|<servername>|all");
|
||||
case AllowInvites:
|
||||
return QStringLiteral("/allowinvites <@userid>|<!roomid>|<servername>|all");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
|
@ -271,6 +279,12 @@ CommandCompleter::data(const QModelIndex &index, int role) const
|
|||
return tr("Ignore a user.");
|
||||
case Unignore:
|
||||
return tr("Stop ignoring a user.");
|
||||
case BlockInvites:
|
||||
return tr("Block all invites from a user, a server, to a specific room or set the "
|
||||
"default.");
|
||||
case AllowInvites:
|
||||
return tr("Allow all invites from a user, a server, to a specific room or set the "
|
||||
"default.");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ public:
|
|||
ConvertToRoom,
|
||||
Ignore,
|
||||
Unignore,
|
||||
BlockInvites,
|
||||
AllowInvites,
|
||||
COUNT,
|
||||
};
|
||||
|
||||
|
|
|
|||
116
src/Utils.cpp
116
src/Utils.cpp
|
|
@ -2114,3 +2114,119 @@ utils::graduallyGlitchText(const QString &text)
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
static QString
|
||||
mxidFromSegments(QStringView sigil, QStringView mxid)
|
||||
{
|
||||
if (mxid.isEmpty())
|
||||
return QString();
|
||||
|
||||
auto mxid_ = QUrl::fromPercentEncoding(mxid.toUtf8());
|
||||
|
||||
if (sigil == u"u") {
|
||||
return "@" + mxid_;
|
||||
} else if (sigil == u"roomid") {
|
||||
return "!" + mxid_;
|
||||
} else if (sigil == u"r") {
|
||||
return "#" + mxid_;
|
||||
//} else if (sigil == "group") {
|
||||
// return "+" + mxid_;
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<utils::MatrixUriParseResult>
|
||||
utils::parseMatrixUri(QString uri)
|
||||
{
|
||||
QUrl uri_{uri};
|
||||
|
||||
// Convert matrix.to URIs to proper format
|
||||
if (uri_.scheme() == QLatin1String("https") && uri_.host() == QLatin1String("matrix.to")) {
|
||||
QString p = uri_.fragment(QUrl::FullyEncoded);
|
||||
if (p.startsWith(QLatin1String("/")))
|
||||
p.remove(0, 1);
|
||||
|
||||
auto temp = p.split(QStringLiteral("?"));
|
||||
QString query;
|
||||
if (temp.size() >= 2)
|
||||
query = QUrl::fromPercentEncoding(temp.takeAt(1).toUtf8());
|
||||
|
||||
temp = temp.first().split(QStringLiteral("/"));
|
||||
auto identifier = QUrl::fromPercentEncoding(temp.takeFirst().toUtf8());
|
||||
QString eventId = QUrl::fromPercentEncoding(temp.join('/').toUtf8());
|
||||
if (!identifier.isEmpty()) {
|
||||
if (identifier.startsWith(QLatin1String("@"))) {
|
||||
QByteArray newUri = "matrix:u/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
uri_ = QUrl::fromEncoded(newUri);
|
||||
} else if (identifier.startsWith(QLatin1String("#"))) {
|
||||
QByteArray newUri = "matrix:r/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!eventId.isEmpty())
|
||||
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
uri_ = QUrl::fromEncoded(newUri);
|
||||
} else if (identifier.startsWith(QLatin1String("!"))) {
|
||||
QByteArray newUri =
|
||||
"matrix:roomid/" + QUrl::toPercentEncoding(identifier.remove(0, 1));
|
||||
if (!eventId.isEmpty())
|
||||
newUri.append("/e/" + QUrl::toPercentEncoding(eventId.remove(0, 1)));
|
||||
if (!query.isEmpty())
|
||||
newUri.append("?" + query.toUtf8());
|
||||
uri_ = QUrl::fromEncoded(newUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// non-matrix URIs are not handled by us, return false
|
||||
if (uri_.scheme() != QLatin1String("matrix"))
|
||||
return {};
|
||||
|
||||
auto tempPath = uri_.path(QUrl::ComponentFormattingOption::FullyEncoded);
|
||||
if (tempPath.startsWith('/'))
|
||||
tempPath.remove(0, 1);
|
||||
auto segments = QStringView(tempPath).split('/');
|
||||
|
||||
if (segments.size() != 2 && segments.size() != 4)
|
||||
return {};
|
||||
|
||||
auto sigil1 = segments[0];
|
||||
auto mxid1 = mxidFromSegments(sigil1, segments[1]);
|
||||
if (mxid1.isEmpty())
|
||||
return {};
|
||||
|
||||
QString mxid2;
|
||||
QString sigil2;
|
||||
if (segments.size() == 4 && segments[2] == QStringView(u"e")) {
|
||||
if (segments[3].isEmpty())
|
||||
return {};
|
||||
else
|
||||
mxid2 = "$" + QUrl::fromPercentEncoding(segments[3].toUtf8());
|
||||
sigil2 = "$";
|
||||
}
|
||||
|
||||
std::vector<std::string> vias;
|
||||
QString action;
|
||||
|
||||
auto items =
|
||||
uri_.query(QUrl::ComponentFormattingOption::FullyEncoded).split('&', Qt::SkipEmptyParts);
|
||||
for (QString item : std::as_const(items)) {
|
||||
if (item.startsWith(QLatin1String("action="))) {
|
||||
action = item.remove(QStringLiteral("action="));
|
||||
} else if (item.startsWith(QLatin1String("via="))) {
|
||||
vias.push_back(QUrl::fromPercentEncoding(item.remove(QStringLiteral("via=")).toUtf8())
|
||||
.toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
return MatrixUriParseResult{
|
||||
.sigil1 = sigil1.toString(),
|
||||
.mxid1 = std::move(mxid1),
|
||||
.sigil2 = std::move(sigil2),
|
||||
.mxid2 = std::move(mxid2),
|
||||
.action = std::move(action),
|
||||
.vias = std::move(vias),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,4 +206,13 @@ glitchText(const QString &text);
|
|||
|
||||
QString
|
||||
graduallyGlitchText(const QString &text);
|
||||
|
||||
struct MatrixUriParseResult
|
||||
{
|
||||
QString sigil1, mxid1, sigil2, mxid2, action;
|
||||
std::vector<std::string> vias;
|
||||
};
|
||||
|
||||
std::optional<MatrixUriParseResult>
|
||||
parseMatrixUri(QString uri);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,9 @@ InputBar::updateTextContentProperties(const QString &t, bool charDeleted)
|
|||
QStringLiteral("converttodm"),
|
||||
QStringLiteral("converttoroom"),
|
||||
QStringLiteral("ignore"),
|
||||
QStringLiteral("unignore")};
|
||||
QStringLiteral("unignore"),
|
||||
QStringLiteral("blockinvites"),
|
||||
QStringLiteral("allowinvites")};
|
||||
bool hasInvalidCommand = !commandName.isNull() && !validCommands.contains(commandName);
|
||||
bool hasIncompleteCommand = hasInvalidCommand && '/' + commandName == t;
|
||||
|
||||
|
|
@ -1025,6 +1027,10 @@ InputBar::command(const QString &command, QString args)
|
|||
this->toggleIgnore(args.trimmed(), true);
|
||||
} else if (command == QLatin1String("unignore")) {
|
||||
this->toggleIgnore(args.trimmed(), false);
|
||||
} else if (command == QLatin1String("blockinvites")) {
|
||||
this->toggleInvitePermission(args.trimmed(), true);
|
||||
} else if (command == QLatin1String("allowinvites")) {
|
||||
this->toggleInvitePermission(args.trimmed(), false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1056,6 +1062,78 @@ InputBar::toggleIgnore(const QString &user, const bool ignored)
|
|||
});
|
||||
}
|
||||
|
||||
void
|
||||
InputBar::toggleInvitePermission(const QString &id, bool block)
|
||||
{
|
||||
mtx::events::account_data::nheko_extensions::InvitePermissions permissions;
|
||||
if (auto ev = cache::client()->getAccountData(mtx::events::EventType::NhekoInvitePermissions)) {
|
||||
permissions = std::get<mtx::events::AccountDataEvent<
|
||||
mtx::events::account_data::nheko_extensions::InvitePermissions>>(*ev)
|
||||
.content;
|
||||
}
|
||||
|
||||
auto idstr = id.toStdString();
|
||||
|
||||
if (id.startsWith("matrix:") || id.startsWith("https://matrix.to")) {
|
||||
auto m = utils::parseMatrixUri(id);
|
||||
if (m) {
|
||||
idstr = m->mxid1.toStdString();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (idstr.starts_with("@")) {
|
||||
if (block) {
|
||||
permissions.user_allow.erase(idstr);
|
||||
permissions.user_deny.emplace(idstr, "{}");
|
||||
} else {
|
||||
permissions.user_deny.erase(idstr);
|
||||
permissions.user_allow.emplace(idstr, "{}");
|
||||
}
|
||||
} else if (idstr.starts_with("!")) {
|
||||
if (block) {
|
||||
permissions.room_allow.erase(idstr);
|
||||
permissions.room_deny.emplace(idstr, "{}");
|
||||
} else {
|
||||
permissions.room_deny.erase(idstr);
|
||||
permissions.room_allow.emplace(idstr, "{}");
|
||||
}
|
||||
} else if (idstr == "all" || idstr == "default") {
|
||||
if (block)
|
||||
permissions.default_ = "deny";
|
||||
else
|
||||
permissions.default_ = "allow";
|
||||
} else if (!idstr.starts_with("#")) {
|
||||
if (block) {
|
||||
permissions.server_allow.erase(idstr);
|
||||
permissions.server_deny.emplace(idstr, "{}");
|
||||
} else {
|
||||
permissions.server_deny.erase(idstr);
|
||||
permissions.server_allow.emplace(idstr, "{}");
|
||||
}
|
||||
}
|
||||
|
||||
http::client()->put_account_data(permissions, [](mtx::http::RequestErr err) {
|
||||
if (err) {
|
||||
nhlog::ui()->error("Failed to update invite permissions: {}", *err);
|
||||
}
|
||||
});
|
||||
|
||||
auto invites = cache::client()->invites();
|
||||
|
||||
for (const auto &[roomid, info] : invites.asKeyValueRange()) {
|
||||
auto roomid_ = roomid.toStdString();
|
||||
auto self =
|
||||
cache::client()->getInviteMember(roomid_, http::client()->user_id().to_string());
|
||||
if (!self->inviter.empty()) {
|
||||
if (!permissions.invite_allowed(roomid_, self->inviter)) {
|
||||
ChatPage::instance()->leaveRoom(roomid, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MediaUpload::MediaUpload(std::unique_ptr<QIODevice> source_,
|
||||
const QString &mimetype,
|
||||
const QString &originalFilename,
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ private:
|
|||
void updateTextContentProperties(const QString &t, bool textDeleted = false);
|
||||
|
||||
void toggleIgnore(const QString &user, const bool ignored);
|
||||
void toggleInvitePermission(const QString &id, bool block);
|
||||
|
||||
QTimer typingRefresh_;
|
||||
QTimer typingTimeout_;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue