Room version 12 and room upgrades
This commit is contained in:
@@ -447,9 +447,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
||||
let writtenMentionMatch = content.match(/(?:^|[^"[<>/A-Za-z0-9])@([A-Za-z][A-Za-z0-9._\[\]\(\)-]+):?/d) // /d flag for indices requires node.js 16+
|
||||
if (writtenMentionMatch) {
|
||||
if (writtenMentionMatch[1] === "room") { // convert @room to @everyone
|
||||
const powerLevels = await di.api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
const userPower = powerLevels.users?.[senderMxid] || 0
|
||||
if (userPower >= powerLevels.notifications?.room) {
|
||||
const {powers: {[senderMxid]: userPower}, powerLevels} = await mxUtils.getEffectivePower(roomID, [senderMxid], di.api)
|
||||
if (userPower >= (powerLevels.notifications?.room ?? 50)) {
|
||||
return {
|
||||
// @ts-ignore - typescript doesn't know about indices yet
|
||||
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||
@@ -924,7 +923,6 @@ async function eventToMessage(event, guild, di) {
|
||||
|
||||
// Respect sender's angle brackets
|
||||
const alreadySuppressed = content[match.index-1+offset] === "<" && content[match.index+match.length+offset] === ">"
|
||||
console.error(content, match.index-1+offset, content[match.index-1+offset])
|
||||
if (alreadySuppressed) continue
|
||||
// Put < > around any surviving matrix.to links
|
||||
let shouldSuppress = !!match[0].match(/^https?:\/\/matrix\.to\//)
|
||||
|
||||
@@ -4985,7 +4985,7 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
||||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
@@ -4996,6 +4996,19 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
||||
room: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -5016,7 +5029,6 @@ test("event2message: @room converts to @everyone and is allowed when the room do
|
||||
})
|
||||
|
||||
test("event2message: @room converts to @everyone but is not allowed when the room restricts who can use it", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
@@ -5031,8 +5043,7 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
||||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
async getStateEvent(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
@@ -5042,6 +5053,19 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
||||
room: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -5062,7 +5086,6 @@ test("event2message: @room converts to @everyone but is not allowed when the roo
|
||||
})
|
||||
|
||||
test("event2message: @room converts to @everyone and is allowed if the user has sufficient power to use it", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
@@ -5077,8 +5100,7 @@ test("event2message: @room converts to @everyone and is allowed if the user has
|
||||
event_id: "$SiXetU9h9Dg-M9Frcw_C6ahnoXZ3QPZe3MVJR5tcB9A"
|
||||
}, data.guild.general, {
|
||||
api: {
|
||||
getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
async getStateEvent(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
@@ -5090,6 +5112,19 @@ test("event2message: @room converts to @everyone and is allowed if the user has
|
||||
room: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(type, "m.room.create")
|
||||
t.equal(key, "")
|
||||
return {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: "@_ooye_bot:cadence.moe",
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -129,7 +129,7 @@ class MatrixStringBuilder {
|
||||
* https://spec.matrix.org/v1.9/appendices/#routing
|
||||
* https://gitdab.com/cadence/out-of-your-element/issues/11
|
||||
* @param {string} roomID
|
||||
* @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]}} api
|
||||
* @param {{[K in "getStateEvent" | "getStateEventOuter" | "getJoinedMembers"]: import("../../matrix/api")[K]} | {getEffectivePower: (roomID: string, mxids: string[], api: any) => Promise<{powers: Record<string, number>, allCreators: string[], tombstone: number, roomCreate: Ty.Event.StateOuter<Ty.Event.M_Room_Create>, powerLevels: Ty.Event.M_Power_Levels}>, getJoinedMembers: import("../../matrix/api")["getJoinedMembers"]}} api
|
||||
*/
|
||||
async function getViaServers(roomID, api) {
|
||||
const candidates = []
|
||||
@@ -138,10 +138,10 @@ async function getViaServers(roomID, api) {
|
||||
candidates.push(reg.ooye.server_name)
|
||||
// Candidate 1: Highest joined non-sim non-bot power level user in the room
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/552c65db98b59406fb49562e537a2721c8505517/src/utils/permalinks/Permalinks.ts#L172
|
||||
const {allCreators, powerLevels} = await getEffectivePower(roomID, [bot], api)
|
||||
const call = "getEffectivePower" in api ? api.getEffectivePower(roomID, [bot], api) : getEffectivePower(roomID, [bot], api)
|
||||
const {allCreators, powerLevels} = await call
|
||||
const sorted = allCreators.concat(Object.entries(powerLevels.users ?? {}).sort((a, b) => b[1] - a[1]).map(([mxid]) => mxid)) // Highest...
|
||||
for (const power of sorted) {
|
||||
const mxid = power[0]
|
||||
for (const mxid of sorted) {
|
||||
if (!(mxid in joined)) continue // joined...
|
||||
if (userRegex.some(r => mxid.match(r))) continue // non-sim non-bot...
|
||||
const match = mxid.match(/:(.*)/)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const e = new Error("Custom error")
|
||||
|
||||
const {test} = require("supertape")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers} = require("./utils")
|
||||
const {eventSenderIsFromDiscord, getEventIDHash, MatrixStringBuilder, getViaServers, roomHasAtLeastVersion} = require("./utils")
|
||||
const util = require("util")
|
||||
|
||||
/** @param {string[]} mxids */
|
||||
@@ -88,9 +88,42 @@ test("MatrixStringBuilder: complete code coverage", t => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string[]} [creators]
|
||||
* @param {{[x: string]: number}} [users]
|
||||
* @param {string} [roomVersion]
|
||||
*/
|
||||
function mockGetEffectivePower(creators = ["@_ooye_bot:cadence.moe"], users = {}, roomVersion = "12") {
|
||||
return async function getEffectivePower(roomID, mxids) {
|
||||
return {
|
||||
allCreators: creators,
|
||||
powerLevels: {users},
|
||||
powers: mxids.reduce((a, mxid) => {
|
||||
if (creators.includes(mxid) && roomHasAtLeastVersion(roomVersion, 12)) a[mxid] = Infinity
|
||||
else if (mxid in users) a[mxid] = users[mxid]
|
||||
else a[mxid] = 0
|
||||
return a
|
||||
}, {}),
|
||||
roomCreate: {
|
||||
type: "m.room.create",
|
||||
state_key: "",
|
||||
sender: creators[0],
|
||||
content: {
|
||||
additional_creators: creators.slice(1),
|
||||
room_version: roomVersion
|
||||
},
|
||||
room_id: roomID,
|
||||
origin_server_ts: 0,
|
||||
event_id: "$create"
|
||||
},
|
||||
tombstone: roomVersion === "12" ? 150 : 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("getViaServers: returns the server name if the room only has sim users", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe"])
|
||||
@@ -98,7 +131,7 @@ test("getViaServers: returns the server name if the room only has sim users", as
|
||||
|
||||
test("getViaServers: also returns the most popular servers in order", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "thecollective.invalid", "selfhosted.invalid"])
|
||||
@@ -106,20 +139,27 @@ test("getViaServers: also returns the most popular servers in order", async t =>
|
||||
|
||||
test("getViaServers: does not return IP address servers", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({}),
|
||||
getEffectivePower: mockGetEffectivePower(),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:45.77.232.172:8443", "@cadence:[::1]:8443", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "123example.456example.invalid"])
|
||||
})
|
||||
|
||||
test("getViaServers: also returns the highest power level user (v12 creator)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@singleuser:selfhosted.invalid"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "selfhosted.invalid", "thecollective.invalid", "tractor.invalid"])
|
||||
})
|
||||
|
||||
test("getViaServers: also returns the highest power level user (100)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@moderator:tractor.invalid"])
|
||||
})
|
||||
@@ -128,11 +168,8 @@ test("getViaServers: also returns the highest power level user (100)", async t =
|
||||
|
||||
test("getViaServers: also returns the highest power level user (50)", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
|
||||
})
|
||||
@@ -141,38 +178,23 @@ test("getViaServers: also returns the highest power level user (50)", async t =>
|
||||
|
||||
test("getViaServers: returns at most 4 results", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe"], {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@singleuser:selfhosted.invalid": 100
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result.length, 4)
|
||||
})
|
||||
|
||||
test("getViaServers: returns results even when power levels can't be fetched", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => {
|
||||
throw new Error("event not found or something")
|
||||
},
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@singleuser:selfhosted.invalid", "@hazel:thecollective.invalid", "@cadence:123example.456example.invalid"])
|
||||
})
|
||||
t.deepEqual(result.length, 4)
|
||||
})
|
||||
|
||||
test("getViaServers: only considers power levels of currently joined members", async t => {
|
||||
const result = await getViaServers("!baby", {
|
||||
getStateEvent: async () => ({
|
||||
users: {
|
||||
"@moderator:tractor.invalid": 50,
|
||||
"@former_moderator:missing.invalid": 100,
|
||||
"@_ooye_bot:cadence.moe": 100
|
||||
}
|
||||
getEffectivePower: mockGetEffectivePower(["@_ooye_bot:cadence.moe", "@former_moderator:missing.invalid"], {
|
||||
"@moderator:tractor.invalid": 50
|
||||
}),
|
||||
getJoinedMembers: async () => joinedList(["@_ooye_bot:cadence.moe", "@_ooye_hazel:cadence.moe", "@cadence:cadence.moe", "@moderator:tractor.invalid", "@hazel:thecollective.invalid", "@june:thecollective.invalid", "@singleuser:selfhosted.invalid"])
|
||||
})
|
||||
t.deepEqual(result, ["cadence.moe", "tractor.invalid", "thecollective.invalid", "selfhosted.invalid"])
|
||||
})
|
||||
|
||||
module.exports.mockGetEffectivePower = mockGetEffectivePower
|
||||
|
||||
@@ -26,6 +26,8 @@ const utils = sync.require("./converters/utils")
|
||||
const api = sync.require("../matrix/api")
|
||||
/** @type {import("../d2m/actions/create-room")} */
|
||||
const createRoom = sync.require("../d2m/actions/create-room")
|
||||
/** @type {import("../matrix/room-upgrade")} */
|
||||
const roomUpgrade = require("../matrix/room-upgrade")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
let lastReportedEvent = 0
|
||||
@@ -171,9 +173,8 @@ async function onRetryReactionAdd(reactionEvent) {
|
||||
// To stop people injecting misleading messages, the reaction needs to come from either the original sender or a room moderator
|
||||
if (reactionEvent.sender !== event.sender) {
|
||||
// Check if it's a room moderator
|
||||
const powerLevelsStateContent = await api.getStateEvent(roomID, "m.room.power_levels", "")
|
||||
const powerLevel = powerLevelsStateContent.users?.[reactionEvent.sender] || 0
|
||||
if (powerLevel < 50) return
|
||||
const {powers: {[reactionEvent.sender]: senderPower}, powerLevels} = await utils.getEffectivePower(roomID, [reactionEvent.sender], api)
|
||||
if (senderPower < (powerLevels.state_default ?? 50)) return
|
||||
}
|
||||
|
||||
// Retry
|
||||
@@ -330,6 +331,11 @@ async event => {
|
||||
if (event.state_key[0] !== "@") return
|
||||
const bot = `@${reg.sender_localpart}:${reg.ooye.server_name}`
|
||||
|
||||
if (event.state_key === bot) {
|
||||
const upgraded = await roomUpgrade.onBotMembership(event)
|
||||
if (upgraded) return
|
||||
}
|
||||
|
||||
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."
|
||||
@@ -342,10 +348,10 @@ async event => {
|
||||
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")
|
||||
const name = getFromInviteRoomState(inviteRoomState, "m.room.name", "name")
|
||||
const topic = getFromInviteRoomState(inviteRoomState, "m.room.topic", "topic")
|
||||
const avatar = getFromInviteRoomState(inviteRoomState, "m.room.avatar", "url")
|
||||
const creationType = getFromInviteRoomState(inviteRoomState, "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! (${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)
|
||||
@@ -368,18 +374,14 @@ async event => {
|
||||
if (!exists) return // don't cache members in unbridged rooms
|
||||
|
||||
// Member is here
|
||||
let powerLevel = 0
|
||||
try {
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelsEvent = await api.getStateEvent(event.room_id, "m.room.power_levels", "")
|
||||
powerLevel = powerLevelsEvent.users?.[event.state_key] ?? powerLevelsEvent.users_default ?? 0
|
||||
} catch (e) {}
|
||||
let {powers: {[event.state_key]: memberPower}, tombstone} = await utils.getEffectivePower(event.room_id, [event.state_key], api)
|
||||
if (memberPower === Infinity) memberPower = tombstone // database storage compatibility
|
||||
const displayname = event.content.displayname || null
|
||||
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(
|
||||
event.room_id, event.state_key,
|
||||
displayname, avatar_url, powerLevel,
|
||||
displayname, avatar_url, powerLevel
|
||||
displayname, avatar_url, memberPower,
|
||||
displayname, avatar_url, memberPower
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -390,11 +392,22 @@ sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_le
|
||||
async event => {
|
||||
if (event.state_key !== "") return
|
||||
const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all()
|
||||
const {allCreators} = await utils.getEffectivePower(event.room_id, [], api)
|
||||
const newPower = event.content.users || {}
|
||||
for (const mxid of existingPower) {
|
||||
db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(newPower[mxid] || 0, event.room_id, mxid)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:m.room.tombstone", guard("m.room.tombstone",
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Tombstone>} event
|
||||
*/
|
||||
async event => {
|
||||
await roomUpgrade.onTombstone(event)
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
module.exports.sendError = sendError
|
||||
|
||||
Reference in New Issue
Block a user