More consistency for invite records table
- Autojoined child spaces are recorded as invited - Update entry when reinvited - Delete entry when uninvited or removed from room - Allow linking with spaces you moderate, even if you didn't invite - Store power levels immediately for new invited rooms - Mark members as missing profile in this case - Only delete from invite table if it left the space
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE member_cache ADD COLUMN missing_profile INTEGER;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Vendored
+2
-1
@@ -90,6 +90,7 @@ export type Models = {
|
|||||||
displayname: string | null
|
displayname: string | null
|
||||||
avatar_url: string | null,
|
avatar_url: string | null,
|
||||||
power_level: number
|
power_level: number
|
||||||
|
missing_profile: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
member_power: {
|
member_power: {
|
||||||
@@ -146,7 +147,7 @@ export type Models = {
|
|||||||
question_text: string
|
question_text: string
|
||||||
is_closed: number
|
is_closed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
poll_option: {
|
poll_option: {
|
||||||
message_id: string
|
message_id: string
|
||||||
matrix_option: string
|
matrix_option: string
|
||||||
|
|||||||
@@ -290,8 +290,8 @@ function convertEmoji(mxcUrl, nameForGuess, allowSpriteSheetIndicator, allowLink
|
|||||||
* @returns {Promise<{displayname?: string?, avatar_url?: string?}>}
|
* @returns {Promise<{displayname?: string?, avatar_url?: string?}>}
|
||||||
*/
|
*/
|
||||||
async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
|
async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
|
||||||
const row = select("member_cache", ["displayname", "avatar_url"], {room_id: roomID, mxid}).get()
|
const row = select("member_cache", ["displayname", "avatar_url", "missing_profile"], {room_id: roomID, mxid}).get()
|
||||||
if (row) return row
|
if (row && !row.missing_profile) return row
|
||||||
return api.getStateEvent(roomID, "m.room.member", mxid).then(event => {
|
return api.getStateEvent(roomID, "m.room.member", mxid).then(event => {
|
||||||
const room = select("channel_room", "room_id", {room_id: roomID}).get()
|
const room = select("channel_room", "room_id", {room_id: roomID}).get()
|
||||||
if (room) {
|
if (room) {
|
||||||
@@ -299,7 +299,7 @@ async function getMemberFromCacheOrHomeserver(roomID, mxid, api) {
|
|||||||
// the cache will be kept in sync by the `m.room.member` event listener
|
// the cache will be kept in sync by the `m.room.member` event listener
|
||||||
const displayname = event?.displayname || null
|
const displayname = event?.displayname || null
|
||||||
const avatar_url = event?.avatar_url || null
|
const avatar_url = event?.avatar_url || null
|
||||||
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?").run(
|
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, missing_profile = NULL").run(
|
||||||
roomID, mxid,
|
roomID, mxid,
|
||||||
displayname, avatar_url,
|
displayname, avatar_url,
|
||||||
displayname, avatar_url
|
displayname, avatar_url
|
||||||
|
|||||||
+36
-14
@@ -371,7 +371,18 @@ sync.addTemporaryListener(as, "type:m.space.child", guard("m.space.child",
|
|||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (Array.isArray(event.content.via) && event.content.via.length) { // space child is being added
|
if (Array.isArray(event.content.via) && event.content.via.length) { // space child is being added
|
||||||
await api.joinRoom(event.state_key).catch(() => {}) // try to join if able, it's okay if it doesn't want, bot will still respond to invites
|
try {
|
||||||
|
// try to join if able, it's okay if it doesn't want, bot will still respond to invites
|
||||||
|
await api.joinRoom(event.state_key)
|
||||||
|
// if autojoined a child space, store it in invite (otherwise the child space will be impossible to use with self-service in the future)
|
||||||
|
const hierarchy = await api.getHierarchy(event.state_key, {limit: 1})
|
||||||
|
const roomProperties = hierarchy.rooms?.[0]
|
||||||
|
if (roomProperties?.room_id === event.state_key && roomProperties.room_type === "m.space" && roomProperties.name) {
|
||||||
|
db.prepare("INSERT OR IGNORE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)")
|
||||||
|
.run(event.sender, event.state_key, roomProperties.room_type, roomProperties.name, roomProperties.topic, roomProperties.avatar_url)
|
||||||
|
await updateMemberCachePowerLevels(event.state_key) // store privileged users in member_cache so they are also allowed to perform self-service
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -404,22 +415,24 @@ async event => {
|
|||||||
}
|
}
|
||||||
if (!inviteRoomState?.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 (!inviteRoomState?.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.`)
|
||||||
await api.joinRoom(event.room_id)
|
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, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
|
db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
|
||||||
if (inviteRoomState.avatar) utils.getPublicUrlForMxc(inviteRoomState.avatar) // make sure it's available in the media_proxy allowed URLs
|
if (inviteRoomState.avatar) utils.getPublicUrlForMxc(inviteRoomState.avatar) // make sure it's available in the media_proxy allowed URLs
|
||||||
|
await updateMemberCachePowerLevels(event.room_id) // store privileged users in member_cache so they are also allowed to perform self-service
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utils.eventSenderIsFromDiscord(event.state_key)) return
|
|
||||||
|
|
||||||
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
||||||
// Member is gone
|
// Member is gone
|
||||||
db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
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
|
// Unregister room's use as a direct chat and/or an invite target if the bot itself left
|
||||||
if (event.state_key === utils.bot) {
|
if (event.state_key === utils.bot) {
|
||||||
db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id)
|
db.prepare("DELETE FROM direct WHERE room_id = ?").run(event.room_id)
|
||||||
|
db.prepare("DELETE FROM invite WHERE room_id = ?").run(event.room_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (utils.eventSenderIsFromDiscord(event.state_key)) return
|
||||||
|
|
||||||
const exists = select("channel_room", "room_id", {room_id: event.room_id}) ?? select("guild_space", "space_id", {space_id: event.room_id})
|
const exists = select("channel_room", "room_id", {room_id: event.room_id}) ?? select("guild_space", "space_id", {space_id: event.room_id})
|
||||||
if (!exists) return // don't cache members in unbridged rooms
|
if (!exists) return // don't cache members in unbridged rooms
|
||||||
|
|
||||||
@@ -428,7 +441,7 @@ async event => {
|
|||||||
if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
|
if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
|
||||||
const displayname = event.content.displayname || null
|
const displayname = event.content.displayname || null
|
||||||
const avatar_url = event.content.avatar_url
|
const avatar_url = event.content.avatar_url
|
||||||
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?").run(
|
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?, power_level = ?, missing_profile = NULL").run(
|
||||||
event.room_id, event.state_key,
|
event.room_id, event.state_key,
|
||||||
displayname, avatar_url, memberPower,
|
displayname, avatar_url, memberPower,
|
||||||
displayname, avatar_url, memberPower
|
displayname, avatar_url, memberPower
|
||||||
@@ -441,16 +454,25 @@ sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_le
|
|||||||
*/
|
*/
|
||||||
async event => {
|
async event => {
|
||||||
if (event.state_key !== "") return
|
if (event.state_key !== "") return
|
||||||
const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all()
|
await updateMemberCachePowerLevels(event.room_id)
|
||||||
const {allCreators} = await utils.getEffectivePower(event.room_id, [], api)
|
|
||||||
const newPower = event.content.users || {}
|
|
||||||
for (const mxid of existingPower) {
|
|
||||||
if (!allCreators.includes(mxid)) {
|
|
||||||
db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(newPower[mxid] || 0, event.room_id, mxid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} roomID
|
||||||
|
*/
|
||||||
|
async function updateMemberCachePowerLevels(roomID) {
|
||||||
|
const existingPower = select("member_cache", "mxid", {room_id: roomID}).pluck().all()
|
||||||
|
const {powerLevels, allCreators, tombstone} = await utils.getEffectivePower(roomID, [], api)
|
||||||
|
const newPower = powerLevels.users || {}
|
||||||
|
const newPowerUsers = Object.keys(newPower)
|
||||||
|
const relevantUsers = existingPower.concat(newPowerUsers).concat(allCreators)
|
||||||
|
for (const mxid of [...new Set(relevantUsers)]) {
|
||||||
|
const level = allCreators.includes(mxid) ? tombstone : newPower[mxid] ?? powerLevels.users_default ?? 0
|
||||||
|
db.prepare("INSERT INTO member_cache (room_id, mxid, power_level, missing_profile) VALUES (?, ?, ?, 1) ON CONFLICT DO UPDATE SET power_level = ?")
|
||||||
|
.run(roomID, mxid, level, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||||
|
|||||||
@@ -148,7 +148,13 @@ as.router.get("/guild", defineEventHandler(async event => {
|
|||||||
|
|
||||||
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
|
// Self-service guild that hasn't been linked yet - needs a special page encouraging the link flow
|
||||||
if (!row.space_id && row.autocreate === 0) {
|
if (!row.space_id && row.autocreate === 0) {
|
||||||
const spaces = db.prepare("SELECT room_id, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL AND type = 'm.space'").all(session.data.mxid)
|
let spaces =
|
||||||
|
// invited spaces
|
||||||
|
db.prepare("SELECT room_id, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id WHERE mxid = ? AND space_id IS NULL AND type = 'm.space'").all(session.data.mxid)
|
||||||
|
// moderated spaces
|
||||||
|
.concat(db.prepare("SELECT room_id, type, name, topic, avatar FROM invite LEFT JOIN guild_space ON invite.room_id = guild_space.space_id INNER JOIN member_cache USING (room_id) WHERE member_cache.mxid = ? AND power_level >= 50 AND space_id IS NULL AND type = 'm.space'").all(session.data.mxid))
|
||||||
|
const seen = new Set(spaces.map(s => s.room_id))
|
||||||
|
spaces = spaces.filter(s => seen.delete(s.room_id))
|
||||||
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
|
return pugSync.render(event, "guild_not_linked.pug", {guild, guild_id, spaces})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,11 +282,11 @@ as.router.post("/api/unlink-space", defineEventHandler(async event => {
|
|||||||
await utils.setUserPower(spaceID, utils.bot, 0, api)
|
await utils.setUserPower(spaceID, utils.bot, 0, api)
|
||||||
await api.leaveRoom(spaceID)
|
await api.leaveRoom(spaceID)
|
||||||
db.prepare("DELETE FROM guild_space WHERE guild_id = ? AND space_id = ?").run(guild_id, spaceID)
|
db.prepare("DELETE FROM guild_space WHERE guild_id = ? AND space_id = ?").run(guild_id, spaceID)
|
||||||
|
db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as not considered for bridging
|
// Mark as not considered for bridging
|
||||||
db.prepare("DELETE FROM guild_active WHERE guild_id = ?").run(guild_id)
|
db.prepare("DELETE FROM guild_active WHERE guild_id = ?").run(guild_id)
|
||||||
db.prepare("DELETE FROM invite WHERE room_id = ?").run(spaceID)
|
|
||||||
await snow.user.leaveGuild(guild_id)
|
await snow.user.leaveGuild(guild_id)
|
||||||
|
|
||||||
setResponseHeader(event, "HX-Redirect", "/")
|
setResponseHeader(event, "HX-Redirect", "/")
|
||||||
|
|||||||
Reference in New Issue
Block a user