Store invite in database and sync power on startup

This commit is contained in:
Cadence Ember 2024-08-26 01:34:46 +12:00
parent 74632c671c
commit df1296e579
8 changed files with 119 additions and 4 deletions

View file

@ -0,0 +1,14 @@
BEGIN TRANSACTION;
-- the power we want them to have
CREATE TABLE IF NOT EXISTS member_power (
mxid TEXT NOT NULL,
room_id TEXT NOT NULL,
power_level INTEGER NOT NULL,
PRIMARY KEY(mxid, room_id)
) WITHOUT ROWID;
-- the power they have
ALTER TABLE member_cache ADD COLUMN power_level INTEGER NOT NULL DEFAULT 0;
COMMIT;

9
db/orm-defs.d.ts vendored
View file

@ -42,7 +42,14 @@ export type Models = {
room_id: string room_id: string
mxid: string mxid: string
displayname: string | null displayname: string | null
avatar_url: string | null avatar_url: string | null,
power_level: number
}
member_power: {
mxid: string
room_id: string
power_level: number
} }
message_channel: { message_channel: {

View file

@ -44,6 +44,8 @@ class From {
/** @private */ /** @private */
this.cols = [] this.cols = []
/** @private */ /** @private */
this.makeColsSafe = true
/** @private */
this.using = [] this.using = []
/** @private */ /** @private */
this.isPluck = false this.isPluck = false
@ -78,6 +80,12 @@ class From {
return r return r
} }
selectUnsafe(...cols) {
this.cols = cols
this.makeColsSafe = false
return this
}
/** /**
* @template {Col} Select * @template {Col} Select
* @param {Select} col * @param {Select} col
@ -112,7 +120,8 @@ class From {
} }
prepare() { prepare() {
let sql = `SELECT ${this.cols.map(k => `"${k}"`).join(", ")} FROM ${this.tables[0]} ` if (this.makeColsSafe) this.cols = this.cols.map(k => `"${k}"`)
let sql = `SELECT ${this.cols.join(", ")} FROM ${this.tables[0]} `
for (let i = 1; i < this.tables.length; i++) { for (let i = 1; i < this.tables.length; i++) {
const table = this.tables[i] const table = this.tables[i]
const col = this.using[i-1] const col = this.using[i-1]

View file

@ -6,7 +6,7 @@
const util = require("util") const util = require("util")
const Ty = require("../types") const Ty = require("../types")
const {discord, db, sync, as} = require("../passthrough") const {discord, db, sync, as, select} = require("../passthrough")
/** @type {import("./actions/send-event")} */ /** @type {import("./actions/send-event")} */
const sendEvent = sync.require("./actions/send-event") const sendEvent = sync.require("./actions/send-event")
@ -167,5 +167,29 @@ sync.addTemporaryListener(as, "type:m.room.member", guard("m.room.member",
async event => { async event => {
if (event.state_key[0] !== "@") return if (event.state_key[0] !== "@") return
if (utils.eventSenderIsFromDiscord(event.state_key)) return if (utils.eventSenderIsFromDiscord(event.state_key)) return
db.prepare("REPLACE INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?)").run(event.room_id, event.state_key, event.content.displayname || null, event.content.avatar_url || null) if (event.content.membership === "leave" || event.content.membership === "ban") {
// Member is gone
db.prepare("DELETE FROM member_cache WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
} else {
// Member is here
db.prepare("INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES (?, ?, ?, ?) ON CONFLICT DO UPDATE SET displayname = ?, avatar_url = ?")
.run(
event.room_id, event.state_key,
event.content.displayname || null, event.content.avatar_url || null,
event.content.displayname || null, event.content.avatar_url || null
)
}
}))
sync.addTemporaryListener(as, "type:m.room.power_levels", guard("m.room.power_levels",
/**
* @param {Ty.Event.StateOuter<Ty.Event.M_Power_Levels>} event
*/
async event => {
if (event.state_key !== "") return
const existingPower = select("member_cache", "mxid", {room_id: event.room_id}).pluck().all()
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)
}
})) }))

View file

@ -70,6 +70,7 @@ async function inviteToRoom(roomID, mxidToInvite, mxid) {
} }
async function leaveRoom(roomID, mxid) { async function leaveRoom(roomID, mxid) {
console.log(`[api] leave: ${roomID}: ${mxid}`)
await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {}) await mreq.mreq("POST", path(`/client/v3/rooms/${roomID}/leave`, mxid), {})
} }

29
matrix/power.js Normal file
View file

@ -0,0 +1,29 @@
// @ts-check
const {db, from} = require("../passthrough")
const api = require("./api")
const reg = require("./read-registration")
const ks = require("./kstate")
const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room")
// Migrate reg.ooye.invite setting to database
for (const mxid of reg.ooye.invite) {
db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*")
}
// Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet.
const rows = from("member_cache").join("member_power", "mxid")
.and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level")
.selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level")
.all()
;(async () => {
for (const row of rows) {
const kstate = await roomToKState(row.room_id)
const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}})
await applyKStateDiffToRoom(row.room_id, diff)
// There is a listener on m.room.power_levels to do this same update,
// but we update it here anyway since the homeserver does not always deliver the event round-trip.
db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid)
}
})()

View file

@ -34,6 +34,7 @@ discord.snow.requestHandler.on("requestError", data => {
await migrate.migrate(db) await migrate.migrate(db)
await discord.cloud.connect() await discord.cloud.connect()
console.log("Discord gateway started") console.log("Discord gateway started")
require("./matrix/power.js")
require("./stdin") require("./stdin")
})() })()

30
types.d.ts vendored
View file

@ -209,6 +209,36 @@ export namespace Event {
name?: string name?: string
} }
export type M_Power_Levels = {
/** The level required to ban a user. Defaults to 50 if unspecified. */
ban?: number,
/** The level required to send specific event types. This is a mapping from event type to power level required. */
events?: {
[event_id: string]: number
},
/** The default level required to send message events. Can be overridden by the `events` key. Defaults to 0 if unspecified. */
events_default?: number,
/** The level required to invite a user. Defaults to 0 if unspecified. */
invite?: number,
/** The level required to kick a user. Defaults to 50 if unspecified. */
kick?: number,
/** The power level requirements for specific notification types. This is a mapping from `key` to power level for that notifications key. */
notifications?: {
room: number,
[key: string]: number
},
/** The level required to redact an event sent by another user. Defaults to 50 if unspecified. */
redact?: number,
/** The default level required to send state events. Can be overridden by the `events` key. Defaults to 50 if unspecified. */
state_default?: number,
/** The power levels for specific users. This is a mapping from `user_id` to power level for that user. */
users?: {
[mxid: string]: number
},
/**The power level for users in the room whose `user_id` is not mentioned in the `users` key. Defaults to 0 if unspecified. */
users_default?: number
}
export type M_Reaction = { export type M_Reaction = {
"m.relates_to": { "m.relates_to": {
rel_type: "m.annotation" rel_type: "m.annotation"