Maybe accept invites more reliably

This commit is contained in:
Cadence Ember
2025-11-27 21:48:49 +13:00
parent 1758b7aa22
commit f176b547ce
4 changed files with 114 additions and 3 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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<Ty.Event.InviteStrippedState[]>}
*/
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

81
src/types.d.ts vendored
View File

@@ -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<any>[]
timeline_events?: Event.Outer<any>[]
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<T> = {
chunk: T[]
next_batch?: string