Implement MSC 4228 Search Redirection

See https://github.com/matrix-org/matrix-spec-proposals/pull/4228 for details.
Since this is tricky to test without server-side support, I have added a basic implementation
to the mock server in appiumtests/login-server.py

1. Start appiumtests/login-server.py
2. Start neochat with "--test --ignore-ssl-errors" options
3. Open "Explore Rooms"
4. Search for the exact string "forbidden"
5. See new error message provided by server
This commit is contained in:
Tobias Fella
2024-11-22 21:53:56 +01:00
parent 9391e44e4b
commit d14d576d99
5 changed files with 70 additions and 4 deletions

View File

@@ -83,6 +83,15 @@ def create_room():
next_sync_payload = "sync_response_new_room" next_sync_payload = "sync_response_new_room"
return response return response
@app.route("/_matrix/client/v3/publicRooms", methods=["POST"])
def public_rooms():
if request.get_json()["filter"]["generic_search_term"] == "forbidden":
data = dict()
data["errcode"] = "M_FORBIDDEN"
data["error"] = "You are not allowed to search for this. Go to https://wikipedia.org for more information"
return data, 403
return dict()
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -8,6 +8,23 @@
using namespace Quotient; using namespace Quotient;
class NeoChatQueryPublicRoomsJob : public QueryPublicRoomsJob
{
public:
explicit NeoChatQueryPublicRoomsJob(const QString &server = {},
std::optional<int> limit = std::nullopt,
const QString &since = {},
const std::optional<Filter> &filter = std::nullopt,
std::optional<bool> includeAllNetworks = std::nullopt,
const QString &thirdPartyInstanceId = {})
: QueryPublicRoomsJob(server, limit, since, filter, includeAllNetworks, thirdPartyInstanceId)
{
// TODO Remove once we can use libQuotient's job directly
// This is to make libQuotient happy about results not having the "chunk" field
setExpectedKeys({});
}
};
PublicRoomListModel::PublicRoomListModel(QObject *parent) PublicRoomListModel::PublicRoomListModel(QObject *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
{ {
@@ -153,6 +170,8 @@ void PublicRoomListModel::next(int limit)
if (m_connection == nullptr || limit < 1) { if (m_connection == nullptr || limit < 1) {
return; return;
} }
m_redirectedText.clear();
Q_EMIT redirectedChanged();
if (job) { if (job) {
qCDebug(PublicRoomList) << "Other job running, ignore"; qCDebug(PublicRoomList) << "Other job running, ignore";
@@ -163,7 +182,7 @@ void PublicRoomListModel::next(int limit)
if (m_showOnlySpaces) { if (m_showOnlySpaces) {
roomTypes += QLatin1String("m.space"); roomTypes += QLatin1String("m.space");
} }
job = m_connection->callApi<QueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes}); job = m_connection->callApi<NeoChatQueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
Q_EMIT searchingChanged(); Q_EMIT searchingChanged();
connect(job, &BaseJob::finished, this, [this] { connect(job, &BaseJob::finished, this, [this] {
@@ -181,6 +200,9 @@ void PublicRoomListModel::next(int limit)
this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1); this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
rooms.append(job->chunk()); rooms.append(job->chunk());
this->endInsertRows(); this->endInsertRows();
} else if (job->error() == BaseJob::ContentAccessError) {
m_redirectedText = job->jsonData()[u"error"_s].toString();
Q_EMIT redirectedChanged();
} }
this->job = nullptr; this->job = nullptr;
@@ -302,4 +324,9 @@ bool PublicRoomListModel::searching() const
return job != nullptr; return job != nullptr;
} }
QString PublicRoomListModel::redirectedText() const
{
return m_redirectedText;
}
#include "moc_publicroomlistmodel.cpp" #include "moc_publicroomlistmodel.cpp"

View File

@@ -52,6 +52,11 @@ class PublicRoomListModel : public QAbstractListModel
*/ */
Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged) Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
/**
* @brief The text returned by the server after redirection
*/
Q_PROPERTY(QString redirectedText READ redirectedText NOTIFY redirectedChanged)
public: public:
/** /**
* @brief Defines the model roles. * @brief Defines the model roles.
@@ -113,6 +118,8 @@ public:
*/ */
Q_INVOKABLE void search(int limit = 50); Q_INVOKABLE void search(int limit = 50);
QString redirectedText() const;
private: private:
QPointer<NeoChatConnection> m_connection = nullptr; QPointer<NeoChatConnection> m_connection = nullptr;
QString m_server; QString m_server;
@@ -135,6 +142,7 @@ private:
QList<Quotient::PublicRoomsChunk> rooms; QList<Quotient::PublicRoomsChunk> rooms;
Quotient::QueryPublicRoomsJob *job = nullptr; Quotient::QueryPublicRoomsJob *job = nullptr;
QString m_redirectedText;
Q_SIGNALS: Q_SIGNALS:
void connectionChanged(); void connectionChanged();
@@ -142,4 +150,5 @@ Q_SIGNALS:
void searchTextChanged(); void searchTextChanged();
void showOnlySpacesChanged(); void showOnlySpacesChanged();
void searchingChanged(); void searchingChanged();
void redirectedChanged();
}; };

View File

@@ -50,6 +50,8 @@ SearchPage {
signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined) signal roomSelected(string roomId, string displayName, url avatarUrl, string alias, string topic, int memberCount, bool isJoined)
title: i18nc("@action:title", "Explore Rooms") title: i18nc("@action:title", "Explore Rooms")
customPlaceholderText: publicRoomListModel.redirectedText
customPlaceholderIcon: "data-warning"
Component.onCompleted: focusSearch() Component.onCompleted: focusSearch()
@@ -93,6 +95,7 @@ SearchPage {
activeFocusOnTab: false // We handle moving to this item via up/down arrows, otherwise the tab order is wacky activeFocusOnTab: false // We handle moving to this item via up/down arrows, otherwise the tab order is wacky
text: i18n("Enter a Room Manually") text: i18n("Enter a Room Manually")
visible: publicRoomListModel.redirectedText.length === 0
icon.name: "compass" icon.name: "compass"
icon.width: Kirigami.Units.gridUnit * 2 icon.width: Kirigami.Units.gridUnit * 2
icon.height: Kirigami.Units.gridUnit * 2 icon.height: Kirigami.Units.gridUnit * 2

View File

@@ -80,6 +80,17 @@ Kirigami.ScrollablePage {
*/ */
property bool showSearchButton: true property bool showSearchButton: true
/**
* @brief Message to be shown in a custom placeholder.
* The custom placeholder will be shown if the text is not empty
*/
property alias customPlaceholderText: customPlaceholder.text
/**
* @brief icon for the custom placeholder
*/
property string customPlaceholderIcon: ""
/** /**
* @brief Force the search field to be focussed. * @brief Force the search field to be focussed.
*/ */
@@ -167,18 +178,25 @@ Kirigami.ScrollablePage {
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
id: noSearchMessage id: noSearchMessage
anchors.centerIn: parent anchors.centerIn: parent
visible: searchField.text.length === 0 && listView.count === 0 visible: searchField.text.length === 0 && listView.count === 0 && !root.showCustomPlaceholder && customPlaceholder.text.length === 0
} }
Kirigami.PlaceholderMessage { Kirigami.PlaceholderMessage {
id: noResultMessage id: noResultMessage
anchors.centerIn: parent anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && customPlaceholder.text.length === 0
}
Kirigami.PlaceholderMessage {
id: customPlaceholder
anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && !root.model.searching && text.length > 0
icon.name: root.customPlaceholderIcon
} }
Kirigami.LoadingPlaceholder { Kirigami.LoadingPlaceholder {
anchors.centerIn: parent anchors.centerIn: parent
visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching visible: searchField.text.length > 0 && listView.count === 0 && root.model.searching && customPlaceholder.text.length === 0
} }
Keys.onUpPressed: { Keys.onUpPressed: {