From f176b547ce2d0ec92e4fbba80fe8f2399d5d836a Mon Sep 17 00:00:00 2001 From: Cadence Ember Date: Thu, 27 Nov 2025 21:48:49 +1300 Subject: [PATCH] Maybe accept invites more reliably --- src/d2m/actions/create-room.js | 1 + src/m2d/event-dispatcher.js | 16 +++++-- src/matrix/api.js | 19 ++++++++ src/types.d.ts | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/src/d2m/actions/create-room.js b/src/d2m/actions/create-room.js index 61e79f3..87c3701 100644 --- a/src/d2m/actions/create-room.js +++ b/src/d2m/actions/create-room.js @@ -568,6 +568,7 @@ module.exports.createAllForGuild = createAllForGuild module.exports.channelToKState = channelToKState module.exports.postApplyPowerLevels = postApplyPowerLevels module.exports._convertNameAndTopic = convertNameAndTopic +module.exports._syncSpaceMember = _syncSpaceMember module.exports.unbridgeChannel = unbridgeChannel module.exports.unbridgeDeletedChannel = unbridgeDeletedChannel module.exports.existsOrAutocreatable = existsOrAutocreatable diff --git a/src/m2d/event-dispatcher.js b/src/m2d/event-dispatcher.js index ce3638c..1f816db 100644 --- a/src/m2d/event-dispatcher.js +++ b/src/m2d/event-dispatcher.js @@ -322,14 +322,25 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member", */ async event => { if (event.state_key[0] !== "@") return + const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}` - if (event.content.membership === "invite" && event.state_key === `@${reg.sender_localpart}:${reg.ooye.server_name}`) { + if (event.content.membership === "invite" && event.state_key === bot) { // We were invited to a room. We should join, and register the invite details for future reference in web. + let attemptedApiMessage = "According to unsigned invite data." + let inviteRoomState = event.unsigned?.invite_room_state + if (!Array.isArray(inviteRoomState) || inviteRoomState.length === 0) { + try { + inviteRoomState = await api.getInviteState(event.room_id) + attemptedApiMessage = "According to SSS API." + } catch (e) { + attemptedApiMessage = "According to unsigned invite data. SSS API unavailable: " + e.toString() + } + } const name = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.name", "name") const topic = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.topic", "topic") const avatar = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.avatar", "url") const creationType = getFromInviteRoomState(event.unsigned?.invite_room_state, "m.room.create", "type") - if (!name) return await api.leaveRoomWithReason(event.room_id, "Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite!") + if (!name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite! (${attemptedApiMessage})`) await api.joinRoom(event.room_id) db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, creationType, name, topic, avatar) if (avatar) utils.getPublicUrlForMxc(avatar) // make sure it's available in the media_proxy allowed URLs @@ -342,7 +353,6 @@ async event => { db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key) // Unregister room's use as a direct chat if the bot itself left - const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}` if (event.state_key === bot) { db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id) } diff --git a/src/matrix/api.js b/src/matrix/api.js index e529d0f..d0892ff 100644 --- a/src/matrix/api.js +++ b/src/matrix/api.js @@ -137,6 +137,24 @@ function getStateEvent(roomID, type, key) { return mreq.mreq("GET", `/client/v3/rooms/${roomID}/state/${type}/${key}`) } +/** + * @param {string} roomID + * @returns {Promise} + */ +async function getInviteState(roomID) { + /** @type {Ty.R.SSS} */ + const root = await mreq.mreq("POST", "/client/unstable/org.matrix.simplified_msc3575/sync", { + room_subscriptions: { + [roomID]: { + timeline_limit: 0, + required_state: [] + } + } + }) + const roomResponse = root.rooms[roomID] + return "stripped_state" in roomResponse ? roomResponse.stripped_state : roomResponse.invite_state +} + /** * "Any of the AS's users must be in the room. This API is primarily for Application Services and should be faster to respond than /members as it can be implemented more efficiently on the server." * @param {string} roomID @@ -483,6 +501,7 @@ module.exports.getEvent = getEvent module.exports.getEventForTimestamp = getEventForTimestamp module.exports.getAllState = getAllState module.exports.getStateEvent = getStateEvent +module.exports.getInviteState = getInviteState module.exports.getJoinedMembers = getJoinedMembers module.exports.getMembers = getMembers module.exports.getHierarchy = getHierarchy diff --git a/src/types.d.ts b/src/types.d.ts index cafd9be..f9488b9 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -166,6 +166,37 @@ export namespace Event { content: any } + export type InviteStrippedState = { + type: string + state_key: string + sender: string + content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias + } + + export type M_Room_Create = { + additional_creators: string[] + "m.federate"?: boolean + room_version: string + type?: string + predecessor?: { + room_id: string + event_id?: string + } + } + + export type M_Room_JoinRules = { + join_rule: "public" | "knock" | "invite" | "private" | "restricted" | "knock_restricted" + allow?: { + type: string + room_id: string + }[] + } + + export type M_Room_CanonicalAlias = { + alias?: string + alt_aliases?: string[] + } + export type M_Room_Message = { msgtype: "m.text" | "m.emote" body: string @@ -375,8 +406,58 @@ export namespace R { room_id: string servers: string[] } + + export type SSS = { + pos: string + lists: { + [list_key: string]: { + count: number + } + } + rooms: { + [room_id: string]: { + bump_stamp: number + /** Omitted if user not in room (peeking) */ + membership?: Membership + /** Names of lists that match this room */ + lists: string[] + } + // If user has been in the room - at least, that's what the spec says. Synapse returns some of these, such as `name` and `avatar`, for invites as well. Go nuts. + & { + name?: string + avatar?: string + heroes?: any[] + /** According to account data */ + is_dm?: boolean + /** If false, omitted fields are unchanged from their previous value. If true, omitted fields means the fields are not set. */ + initial?: boolean + expanded_timeline?: boolean + required_state?: Event.StateOuter[] + timeline_events?: Event.Outer[] + prev_batch?: string + limited?: boolean + num_live?: number + joined_count?: number + invited_count?: number + notification_count?: number + highlight_count?: number + } + // If user is invited or knocked + & ({ + /** @deprecated */ + invite_state: Event.InviteStrippedState[] + } | { + stripped_state: Event.InviteStrippedState[] + }) + } + extensions: { + [extension_key: string]: any + } + } } +export type Membership = "invite" | "knock" | "join" | "leave" | "ban" + export type Pagination = { chunk: T[] next_batch?: string