Fix unfederated invite dialog (#9618)
* clarify error message for unfederated room invites * hide external user suggesetions * rename some descriptors * fix i18n * add warning for unfederated spaces * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
a306a08780
commit
4ff35f0471
6 changed files with 111 additions and 13 deletions
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { createRef, ReactNode, SyntheticEvent } from "react";
|
import React, { createRef, ReactNode, SyntheticEvent } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RoomMember, Room, MatrixError } from "matrix-js-sdk/src/matrix";
|
import { RoomMember, Room, MatrixError, EventType } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
|
@ -368,26 +368,32 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
|
|
||||||
this.profilesStore = SdkContextClass.instance.userProfilesStore;
|
this.profilesStore = SdkContextClass.instance.userProfilesStore;
|
||||||
|
|
||||||
const alreadyInvited = new Set([MatrixClientPeg.safeGet().getUserId()!]);
|
const excludedIds = new Set([MatrixClientPeg.safeGet().getUserId()!]);
|
||||||
const welcomeUserId = SdkConfig.get("welcome_user_id");
|
const welcomeUserId = SdkConfig.get("welcome_user_id");
|
||||||
if (welcomeUserId) alreadyInvited.add(welcomeUserId);
|
if (welcomeUserId) excludedIds.add(welcomeUserId);
|
||||||
|
|
||||||
if (isRoomInvite(props)) {
|
if (isRoomInvite(props)) {
|
||||||
const room = MatrixClientPeg.safeGet().getRoom(props.roomId);
|
const room = MatrixClientPeg.safeGet().getRoom(props.roomId);
|
||||||
|
const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()["m.federate"];
|
||||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||||
room.getMembersWithMembership("invite").forEach((m) => alreadyInvited.add(m.userId));
|
room.getMembersWithMembership("invite").forEach((m) => excludedIds.add(m.userId));
|
||||||
room.getMembersWithMembership("join").forEach((m) => alreadyInvited.add(m.userId));
|
room.getMembersWithMembership("join").forEach((m) => excludedIds.add(m.userId));
|
||||||
// add banned users, so we don't try to invite them
|
// add banned users, so we don't try to invite them
|
||||||
room.getMembersWithMembership("ban").forEach((m) => alreadyInvited.add(m.userId));
|
room.getMembersWithMembership("ban").forEach((m) => excludedIds.add(m.userId));
|
||||||
|
if (isFederated === false) {
|
||||||
|
// exclude users from external servers
|
||||||
|
const homeserver = props.roomId.split(":")[1];
|
||||||
|
this.excludeExternals(homeserver, excludedIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
targets: [], // array of Member objects (see interface above)
|
targets: [], // array of Member objects (see interface above)
|
||||||
filterText: this.props.initialText || "",
|
filterText: this.props.initialText || "",
|
||||||
// Mutates alreadyInvited set so that buildSuggestions doesn't duplicate any users
|
// Mutates alreadyInvited set so that buildSuggestions doesn't duplicate any users
|
||||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
recents: InviteDialog.buildRecents(excludedIds),
|
||||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
suggestions: this.buildSuggestions(alreadyInvited),
|
suggestions: this.buildSuggestions(excludedIds),
|
||||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
serverResultsMixin: [],
|
serverResultsMixin: [],
|
||||||
threepidResultsMixin: [],
|
threepidResultsMixin: [],
|
||||||
|
@ -418,6 +424,18 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
|
||||||
this.setState({ consultFirst: ev.target.checked });
|
this.setState({ consultFirst: ev.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private excludeExternals(homeserver: string, excludedTargetIds: Set<string>): void {
|
||||||
|
const client = MatrixClientPeg.safeGet();
|
||||||
|
// users with room membership
|
||||||
|
const members = Object.values(buildMemberScores(client)).map(({ member }) => member.userId);
|
||||||
|
// users with dm membership
|
||||||
|
const roomMembers = Object.keys(DMRoomMap.shared().getUniqueRoomsWithIndividuals());
|
||||||
|
roomMembers.forEach((id) => members.push(id));
|
||||||
|
// filter duplicates and user IDs from external servers
|
||||||
|
const externals = new Set(members.filter((id) => !id.includes(homeserver)));
|
||||||
|
externals.forEach((id) => excludedTargetIds.add(id));
|
||||||
|
}
|
||||||
|
|
||||||
public static buildRecents(excludedTargetIds: Set<string>): Result[] {
|
public static buildRecents(excludedTargetIds: Set<string>): Result[] {
|
||||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||||
|
|
||||||
|
|
|
@ -1260,6 +1260,8 @@
|
||||||
"error_permissions_space": "You do not have permission to invite people to this space.",
|
"error_permissions_space": "You do not have permission to invite people to this space.",
|
||||||
"error_profile_undisclosed": "User may or may not exist",
|
"error_profile_undisclosed": "User may or may not exist",
|
||||||
"error_transfer_multiple_target": "A call can only be transferred to a single user.",
|
"error_transfer_multiple_target": "A call can only be transferred to a single user.",
|
||||||
|
"error_unfederated_room": "This room is unfederated. You cannot invite people from external servers.",
|
||||||
|
"error_unfederated_space": "This space is unfederated. You cannot invite people from external servers.",
|
||||||
"error_unknown": "Unknown server error",
|
"error_unknown": "Unknown server error",
|
||||||
"error_user_not_found": "User does not exist",
|
"error_user_not_found": "User does not exist",
|
||||||
"error_version_unsupported_room": "The user's homeserver does not support the version of the room.",
|
"error_version_unsupported_room": "The user's homeserver does not support the version of the room.",
|
||||||
|
|
|
@ -246,16 +246,26 @@ export default class MultiInviter {
|
||||||
|
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
const isSpace = this.roomId && this.matrixClient.getRoom(this.roomId)?.isSpaceRoom();
|
const room = this.roomId ? this.matrixClient.getRoom(this.roomId) : null;
|
||||||
|
const isSpace = room?.isSpaceRoom();
|
||||||
|
const isFederated = room?.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[
|
||||||
|
"m.federate"
|
||||||
|
];
|
||||||
|
|
||||||
let errorText: string | undefined;
|
let errorText: string | undefined;
|
||||||
let fatal = false;
|
let fatal = false;
|
||||||
switch (err.errcode) {
|
switch (err.errcode) {
|
||||||
case "M_FORBIDDEN":
|
case "M_FORBIDDEN":
|
||||||
if (isSpace) {
|
if (isSpace) {
|
||||||
errorText = _t("invite|error_permissions_space");
|
errorText =
|
||||||
|
isFederated === false
|
||||||
|
? _t("invite|error_unfederated_space")
|
||||||
|
: _t("invite|error_permissions_space");
|
||||||
} else {
|
} else {
|
||||||
errorText = _t("invite|error_permissions_room");
|
errorText =
|
||||||
|
isFederated === false
|
||||||
|
? _t("invite|error_unfederated_room")
|
||||||
|
: _t("invite|error_permissions_room");
|
||||||
}
|
}
|
||||||
fatal = true;
|
fatal = true;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -477,4 +477,20 @@ describe("InviteDialog", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not suggest users from other server when room has m.federate=false", async () => {
|
||||||
|
SdkConfig.add({ welcome_user_id: "@bot:example.org" });
|
||||||
|
room.currentState.setStateEvents([mkRoomCreateEvent(bobId, roomId, { "m.federate": false })]);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<InviteDialog
|
||||||
|
kind={InviteKind.Invite}
|
||||||
|
roomId={roomId}
|
||||||
|
onFinished={jest.fn()}
|
||||||
|
initialText="@localpart:server.tld"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await flushPromises();
|
||||||
|
expect(screen.queryByText("@localpart:server.tld")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -291,13 +291,14 @@ type MakeEventProps = MakeEventPassThruProps & {
|
||||||
unsigned?: IUnsigned;
|
unsigned?: IUnsigned;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mkRoomCreateEvent = (userId: string, roomId: string): MatrixEvent => {
|
export const mkRoomCreateEvent = (userId: string, roomId: string, content?: IContent): MatrixEvent => {
|
||||||
return mkEvent({
|
return mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: EventType.RoomCreate,
|
type: EventType.RoomCreate,
|
||||||
content: {
|
content: {
|
||||||
creator: userId,
|
creator: userId,
|
||||||
room_version: KNOWN_SAFE_ROOM_VERSION,
|
room_version: KNOWN_SAFE_ROOM_VERSION,
|
||||||
|
...content,
|
||||||
},
|
},
|
||||||
skey: "",
|
skey: "",
|
||||||
user: userId,
|
user: userId,
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { MatrixClient, MatrixError, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { EventType, MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||||
import Modal, { ComponentType, ComponentProps } from "../../src/Modal";
|
import Modal, { ComponentType, ComponentProps } from "../../src/Modal";
|
||||||
|
@ -187,5 +187,56 @@ describe("MultiInviter", () => {
|
||||||
});
|
});
|
||||||
expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1);
|
expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should show sensible error when attempting to invite over federation with m.federate=false", async () => {
|
||||||
|
mocked(client.invite).mockRejectedValueOnce(
|
||||||
|
new MatrixError({
|
||||||
|
errcode: "M_FORBIDDEN",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
||||||
|
room.currentState.setStateEvents([
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomCreate,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
"m.federate": false,
|
||||||
|
},
|
||||||
|
room_id: ROOMID,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
mocked(client.getRoom).mockReturnValue(room);
|
||||||
|
|
||||||
|
await inviter.invite(["@user:other_server"]);
|
||||||
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
||||||
|
`"This room is unfederated. You cannot invite people from external servers."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show sensible error when attempting to invite over federation with m.federate=false to space", async () => {
|
||||||
|
mocked(client.invite).mockRejectedValueOnce(
|
||||||
|
new MatrixError({
|
||||||
|
errcode: "M_FORBIDDEN",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const room = new Room(ROOMID, client, client.getSafeUserId());
|
||||||
|
room.currentState.setStateEvents([
|
||||||
|
new MatrixEvent({
|
||||||
|
type: EventType.RoomCreate,
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
"m.federate": false,
|
||||||
|
"type": "m.space",
|
||||||
|
},
|
||||||
|
room_id: ROOMID,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
mocked(client.getRoom).mockReturnValue(room);
|
||||||
|
|
||||||
|
await inviter.invite(["@user:other_server"]);
|
||||||
|
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
||||||
|
`"This space is unfederated. You cannot invite people from external servers."`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue