Compare commits
2 commits
74632c671c
...
b8793dae0f
Author | SHA1 | Date | |
---|---|---|---|
b8793dae0f | |||
df1296e579 |
12 changed files with 130 additions and 8 deletions
|
@ -142,6 +142,8 @@ async function channelToKState(channel, guild) {
|
||||||
const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites)
|
const everyonePermissions = utils.getPermissions([], guild.roles, undefined, channel.permission_overwrites)
|
||||||
const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
|
const everyoneCanMentionEveryone = utils.hasAllPermissions(everyonePermissions, ["MentionEveryone"])
|
||||||
|
|
||||||
|
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
||||||
|
|
||||||
const channelKState = {
|
const channelKState = {
|
||||||
"m.room.name/": {name: convertedName},
|
"m.room.name/": {name: convertedName},
|
||||||
"m.room.topic/": {topic: convertedTopic},
|
"m.room.topic/": {topic: convertedTopic},
|
||||||
|
@ -161,7 +163,7 @@ async function channelToKState(channel, guild) {
|
||||||
notifications: {
|
notifications: {
|
||||||
room: everyoneCanMentionEveryone ? 0 : 20
|
room: everyoneCanMentionEveryone ? 0 : 20
|
||||||
},
|
},
|
||||||
users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {})
|
users: globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})
|
||||||
},
|
},
|
||||||
"chat.schildi.hide_ui/read_receipts": {
|
"chat.schildi.hide_ui/read_receipts": {
|
||||||
hidden: true
|
hidden: true
|
||||||
|
|
|
@ -31,6 +31,8 @@ async function createSpace(guild, kstate) {
|
||||||
const topic = kstate["m.room.topic/"]?.topic || undefined
|
const topic = kstate["m.room.topic/"]?.topic || undefined
|
||||||
assert(name)
|
assert(name)
|
||||||
|
|
||||||
|
const globalAdmins = select("member_power", "mxid", {room_id: "*"}).pluck().all()
|
||||||
|
|
||||||
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
const roomID = await createRoom.postApplyPowerLevels(kstate, async kstate => {
|
||||||
return api.createRoom({
|
return api.createRoom({
|
||||||
name,
|
name,
|
||||||
|
@ -40,7 +42,7 @@ async function createSpace(guild, kstate) {
|
||||||
events_default: 100, // space can only be managed by bridge
|
events_default: 100, // space can only be managed by bridge
|
||||||
invite: 0 // any existing member can invite others
|
invite: 0 // any existing member can invite others
|
||||||
},
|
},
|
||||||
invite: reg.ooye.invite,
|
invite: globalAdmins,
|
||||||
topic,
|
topic,
|
||||||
creation_content: {
|
creation_content: {
|
||||||
type: "m.space"
|
type: "m.space"
|
||||||
|
@ -58,6 +60,7 @@ async function createSpace(guild, kstate) {
|
||||||
*/
|
*/
|
||||||
async function guildToKState(guild, privacyLevel) {
|
async function guildToKState(guild, privacyLevel) {
|
||||||
assert.equal(typeof privacyLevel, "number")
|
assert.equal(typeof privacyLevel, "number")
|
||||||
|
const globalAdmins = select("member_power", ["mxid", "power_level"], {room_id: "*"}).all()
|
||||||
const guildKState = {
|
const guildKState = {
|
||||||
"m.room.name/": {name: guild.name},
|
"m.room.name/": {name: guild.name},
|
||||||
"m.room.avatar/": {
|
"m.room.avatar/": {
|
||||||
|
@ -68,7 +71,7 @@ async function guildToKState(guild, privacyLevel) {
|
||||||
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
|
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
|
||||||
"m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]},
|
"m.room.history_visibility/": {history_visibility: createRoom.PRIVACY_ENUMS.SPACE_HISTORY_VISIBILITY[privacyLevel]},
|
||||||
"m.room.join_rules/": {join_rule: createRoom.PRIVACY_ENUMS.SPACE_JOIN_RULES[privacyLevel]},
|
"m.room.join_rules/": {join_rule: createRoom.PRIVACY_ENUMS.SPACE_JOIN_RULES[privacyLevel]},
|
||||||
"m.room.power_levels/": {users: reg.ooye.invite.reduce((a, c) => (a[c] = 100, a), {})}
|
"m.room.power_levels/": {users: globalAdmins.reduce((a, c) => (a[c.mxid] = c.power_level, a), {})} // used in guild initial creation postApplyPowerLevels
|
||||||
}
|
}
|
||||||
|
|
||||||
return guildKState
|
return guildKState
|
||||||
|
|
14
db/migrations/0012-add-member-power.sql
Normal file
14
db/migrations/0012-add-member-power.sql
Normal 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
9
db/orm-defs.d.ts
vendored
|
@ -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: {
|
||||||
|
|
11
db/orm.js
11
db/orm.js
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -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
29
matrix/power.js
Normal 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)
|
||||||
|
}
|
||||||
|
})()
|
1
start.js
1
start.js
|
@ -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")
|
||||||
})()
|
})()
|
||||||
|
|
|
@ -139,6 +139,9 @@ INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES
|
||||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
|
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
|
||||||
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL);
|
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL);
|
||||||
|
|
||||||
|
INSERT INTO member_power (mxid, room_id, power_level) VALUES
|
||||||
|
('@test_auto_invite:example.org', '*', 100);
|
||||||
|
|
||||||
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
INSERT INTO lottie (sticker_id, mxc_url) VALUES
|
||||||
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
('860171525772279849', 'mxc://cadence.moe/ZtvvVbwMIdUZeovWVyGVFCeR');
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ const db = new sqlite(":memory:")
|
||||||
const reg = require("../matrix/read-registration")
|
const reg = require("../matrix/read-registration")
|
||||||
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
reg.ooye.server_origin = "https://matrix.cadence.moe" // so that tests will pass even when hard-coded
|
||||||
reg.ooye.server_name = "cadence.moe"
|
reg.ooye.server_name = "cadence.moe"
|
||||||
reg.ooye.invite = ["@test_auto_invite:example.org"]
|
|
||||||
reg.id = "baby" // don't actually take authenticated actions on the server
|
reg.id = "baby" // don't actually take authenticated actions on the server
|
||||||
reg.as_token = "baby"
|
reg.as_token = "baby"
|
||||||
reg.hs_token = "baby"
|
reg.hs_token = "baby"
|
||||||
|
|
30
types.d.ts
vendored
30
types.d.ts
vendored
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue