forked from cadence/out-of-your-element
Compare commits
12 commits
d53e9efe9e
...
8ff61a5499
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ff61a5499 | |||
| 7367fb3b65 | |||
| c75e87f403 | |||
| 8b9d8ec0cc | |||
| 0dac3d2898 | |||
| 9dbd871e0b | |||
| 8c87d93011 | |||
| e8d9a5e4ae | |||
| 876d91fbf4 | |||
| d2557f73bb | |||
| f8896dce7f | |||
| 5b04b5d712 |
27 changed files with 420 additions and 376 deletions
36
scripts/remove-uncached-bridged-users.js
Normal file
36
scripts/remove-uncached-bridged-users.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const HeatSync = require("heatsync")
|
||||||
|
const sync = new HeatSync({watchFS: false})
|
||||||
|
|
||||||
|
const sqlite = require("better-sqlite3")
|
||||||
|
const db = new sqlite("ooye.db", {fileMustExist: true})
|
||||||
|
|
||||||
|
const passthrough = require("../src/passthrough")
|
||||||
|
Object.assign(passthrough, {db, sync})
|
||||||
|
|
||||||
|
const api = require("../src/matrix/api")
|
||||||
|
const utils = require("../src/matrix/utils")
|
||||||
|
const {reg} = require("../src/matrix/read-registration")
|
||||||
|
|
||||||
|
const rooms = db.prepare("select room_id, name, nick from channel_room").all()
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
// Search for members starting with @_ooye_ and kick them if they are not in sim_member cache
|
||||||
|
for (const room of rooms) {
|
||||||
|
try {
|
||||||
|
const members = await api.getJoinedMembers(room.room_id)
|
||||||
|
for (const mxid of Object.keys(members.joined)) {
|
||||||
|
if (!mxid.startsWith("@" + reg.sender_localpart) && utils.eventSenderIsFromDiscord(mxid) && !db.prepare("select mxid from sim_member where mxid = ? and room_id = ?").get(mxid, room.room_id)) {
|
||||||
|
await api.leaveRoom(room.room_id, mxid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes("Appservice not in room")) {
|
||||||
|
// ok
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
@ -256,7 +256,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handling power levels separately. The spec doesn't specify what happens, Dendrite differs,
|
* Handling power levels separately. The spec doesn't specify what happens, Dendrite differs,
|
||||||
* and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates.
|
* and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates.
|
||||||
* We don't want the `events` key to be overridden completely.
|
* We don't want the `events` key to be overridden completely.
|
||||||
* https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
|
* https://github.com/matrix-org/synapse/blob/develop/synapse/handlers/room.py#L1170-L1210
|
||||||
* https://github.com/matrix-org/matrix-spec/issues/492
|
* https://github.com/matrix-org/matrix-spec/issues/492
|
||||||
|
|
|
||||||
|
|
@ -206,14 +206,16 @@ function _hashProfileContent(content, powerLevel) {
|
||||||
* 3. Calculate the power level the user should get based on their Discord permissions
|
* 3. Calculate the power level the user should get based on their Discord permissions
|
||||||
* 4. Compare against the previously known state content, which is helpfully stored in the database
|
* 4. Compare against the previously known state content, which is helpfully stored in the database
|
||||||
* 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time
|
* 5. If the state content or power level have changed, send them to Matrix and update them in the database for next time
|
||||||
|
* 6. If the sim is for a user-installed app, check which user it was added by
|
||||||
* @param {DiscordTypes.APIUser} user
|
* @param {DiscordTypes.APIUser} user
|
||||||
* @param {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
|
* @param {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
|
||||||
* @param {DiscordTypes.APIGuildChannel} channel
|
* @param {DiscordTypes.APIGuildChannel} channel
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
|
* @param {DiscordTypes.APIMessageInteractionMetadata} [interactionMetadata]
|
||||||
* @returns {Promise<string>} mxid of the updated sim
|
* @returns {Promise<string>} mxid of the updated sim
|
||||||
*/
|
*/
|
||||||
async function syncUser(user, member, channel, guild, roomID) {
|
async function syncUser(user, member, channel, guild, roomID, interactionMetadata) {
|
||||||
const mxid = await ensureSimJoined(user, roomID)
|
const mxid = await ensureSimJoined(user, roomID)
|
||||||
const content = await memberToStateContent(user, member, guild.id)
|
const content = await memberToStateContent(user, member, guild.id)
|
||||||
const powerLevel = memberToPowerLevel(user, member, guild, channel)
|
const powerLevel = memberToPowerLevel(user, member, guild, channel)
|
||||||
|
|
@ -222,6 +224,12 @@ async function syncUser(user, member, channel, guild, roomID) {
|
||||||
allowOverwrite: !!member,
|
allowOverwrite: !!member,
|
||||||
globalProfile: await userToGlobalProfile(user)
|
globalProfile: await userToGlobalProfile(user)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const appInstalledByUser = user.bot && interactionMetadata?.authorizing_integration_owners?.[DiscordTypes.ApplicationIntegrationType.UserInstall]
|
||||||
|
if (appInstalledByUser) {
|
||||||
|
db.prepare("INSERT OR IGNORE INTO app_user_install (app_bot_id, user_id, guild_id) VALUES (?, ?, ?)").run(user.id, appInstalledByUser, guild.id)
|
||||||
|
}
|
||||||
|
|
||||||
return mxid
|
return mxid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,124 +1,37 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select, from} = passthrough
|
const {sync, db, select, from} = passthrough
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
|
/** @type {import("../converters/remove-member-mxids")} */
|
||||||
|
const removeMemberMxids = sync.require("../converters/remove-member-mxids")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a specific sim leave all rooms in a guild and clean up the database.
|
* @param {string} userID discord user ID that left
|
||||||
* @param {string} mxid
|
* @param {string} guildID discord guild ID that they left
|
||||||
* @param {string} guildID
|
|
||||||
* @returns {Promise<number>} number of rooms left
|
|
||||||
*/
|
|
||||||
async function removeSimFromGuild(mxid, guildID) {
|
|
||||||
const rooms = from("sim_member")
|
|
||||||
.join("channel_room", "room_id")
|
|
||||||
.where({mxid, guild_id: guildID})
|
|
||||||
.pluck("room_id")
|
|
||||||
.all()
|
|
||||||
|
|
||||||
for (const roomID of rooms) {
|
|
||||||
try {
|
|
||||||
await api.leaveRoom(roomID, mxid)
|
|
||||||
} catch (e) {
|
|
||||||
// Room may no longer exist or sim may already have left
|
|
||||||
console.log(`[remove member] failed to leave room ${roomID}: ${e}`)
|
|
||||||
}
|
|
||||||
db.prepare("DELETE FROM sim_member WHERE mxid = ? AND room_id = ?").run(mxid, roomID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rooms.length
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a user's sim and their PluralKit proxy sims from all rooms in a guild.
|
|
||||||
* Called when a Discord user leaves a guild.
|
|
||||||
* @param {string} userID Discord user ID
|
|
||||||
* @param {string} guildID Discord guild ID
|
|
||||||
*/
|
*/
|
||||||
async function removeMember(userID, guildID) {
|
async function removeMember(userID, guildID) {
|
||||||
// Remove the user's own sim
|
const {userAppDeletions, membership} = removeMemberMxids.removeMemberMxids(userID, guildID)
|
||||||
const mxid = select("sim", "mxid", {user_id: userID}).pluck().get()
|
db.transaction(() => {
|
||||||
if (mxid) {
|
for (const d of userAppDeletions) {
|
||||||
const count = await removeSimFromGuild(mxid, guildID)
|
db.prepare("DELETE FROM app_user_install WHERE guild_id = ? and user_id = ?").run(guildID, d)
|
||||||
if (count) console.log(`[remove member] removed sim for ${userID} from ${count} rooms in guild ${guildID}`)
|
}
|
||||||
}
|
})()
|
||||||
|
for (const m of membership) {
|
||||||
// Remove PluralKit proxy sims owned by this user
|
try {
|
||||||
const pkUserIDs = select("sim_proxy", "user_id", {proxy_owner_id: userID}).pluck().all()
|
await api.leaveRoom(m.room_id, m.mxid)
|
||||||
for (const pkUserID of pkUserIDs) {
|
} catch (e) {
|
||||||
const pkMxid = select("sim", "mxid", {user_id: pkUserID}).pluck().get()
|
if (String(e).includes("not in room")) {
|
||||||
if (!pkMxid) continue
|
// no further action needed
|
||||||
const count = await removeSimFromGuild(pkMxid, guildID)
|
} else {
|
||||||
if (count) console.log(`[remove member] removed pk sim for ${pkUserID} (owner: ${userID}) from ${count} rooms in guild ${guildID}`)
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update cache to say that the member isn't in the room any more
|
||||||
|
// You'd think this would happen automatically when the leave event arrives at Matrix's event dispatcher, but that isn't 100% reliable.
|
||||||
|
db.prepare("DELETE FROM sim_member WHERE room_id = ? AND mxid = ?").run(m.room_id, m.mxid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Backfill: check all sims in guild rooms and remove those whose Discord users have left the guild.
|
|
||||||
* Called on GUILD_CREATE to catch removals that happened while the bridge was offline.
|
|
||||||
* @param {DiscordTypes.GatewayGuildCreateDispatchData} guild
|
|
||||||
*/
|
|
||||||
async function checkMissedMembers(guild) {
|
|
||||||
if (guild.unavailable) return
|
|
||||||
|
|
||||||
// Find all distinct regular Discord user IDs with sims in this guild's rooms
|
|
||||||
// Exclude PK sims (UUIDs with dashes) and webhook sims (with underscores)
|
|
||||||
const rows = from("sim_member")
|
|
||||||
.join("channel_room", "room_id")
|
|
||||||
.join("sim", "mxid")
|
|
||||||
.where({guild_id: guild.id})
|
|
||||||
.and("AND user_id NOT LIKE '%-%' AND user_id NOT LIKE '%\\_%' ESCAPE '\\'")
|
|
||||||
.pluck("user_id")
|
|
||||||
.all()
|
|
||||||
|
|
||||||
const userIDs = [...new Set(rows)]
|
|
||||||
|
|
||||||
for (const userID of userIDs) {
|
|
||||||
try {
|
|
||||||
await discord.snow.guild.getGuildMember(guild.id, userID)
|
|
||||||
} catch (e) {
|
|
||||||
if (String(e).includes("10007") || String(e).includes("Unknown Member")) {
|
|
||||||
console.log(`[remove member] backfill: user ${userID} is no longer in guild ${guild.id}`)
|
|
||||||
await removeMember(userID, guild.id)
|
|
||||||
}
|
|
||||||
// Other errors (rate limits, network issues) - skip this user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check PK proxy owners who may not have a regular sim but whose PK sims are in guild rooms
|
|
||||||
const pkRows = from("sim_member")
|
|
||||||
.join("channel_room", "room_id")
|
|
||||||
.join("sim", "mxid")
|
|
||||||
.join("sim_proxy", "user_id")
|
|
||||||
.where({guild_id: guild.id})
|
|
||||||
.pluck("proxy_owner_id")
|
|
||||||
.all()
|
|
||||||
|
|
||||||
const pkOwnerIDs = [...new Set(pkRows)].filter(id => !userIDs.includes(id))
|
|
||||||
|
|
||||||
for (const ownerID of pkOwnerIDs) {
|
|
||||||
try {
|
|
||||||
await discord.snow.guild.getGuildMember(guild.id, ownerID)
|
|
||||||
} catch (e) {
|
|
||||||
if (String(e).includes("10007") || String(e).includes("Unknown Member")) {
|
|
||||||
console.log(`[remove member] backfill: pk owner ${ownerID} is no longer in guild ${guild.id}`)
|
|
||||||
// Only remove PK sims for this owner, not their own sim (they don't have one in this guild)
|
|
||||||
const pkUserIDs = select("sim_proxy", "user_id", {proxy_owner_id: ownerID}).pluck().all()
|
|
||||||
for (const pkUserID of pkUserIDs) {
|
|
||||||
const pkMxid = select("sim", "mxid", {user_id: pkUserID}).pluck().get()
|
|
||||||
if (!pkMxid) continue
|
|
||||||
const count = await removeSimFromGuild(pkMxid, guild.id)
|
|
||||||
if (count) console.log(`[remove member] backfill: removed pk sim for ${pkUserID} (owner: ${ownerID}) from ${count} rooms in guild ${guild.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.removeSimFromGuild = removeSimFromGuild
|
|
||||||
module.exports.removeMember = removeMember
|
module.exports.removeMember = removeMember
|
||||||
module.exports.checkMissedMembers = checkMissedMembers
|
|
||||||
|
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const {test} = require("supertape")
|
|
||||||
const {removeSimFromGuild, removeMember, checkMissedMembers} = require("./remove-member")
|
|
||||||
const passthrough = require("../../passthrough")
|
|
||||||
const {db, sync, discord} = passthrough
|
|
||||||
|
|
||||||
/** @type {import("../../matrix/api")} */
|
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
|
|
||||||
function setupTestSim(userID, simName, mxid) {
|
|
||||||
db.prepare("INSERT OR IGNORE INTO sim (user_id, username, sim_name, mxid) VALUES (?, ?, ?, ?)").run(userID, simName, simName, mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupTestSimMember(mxid, roomID) {
|
|
||||||
db.prepare("INSERT OR IGNORE INTO sim_member (mxid, room_id, hashed_profile_content) VALUES (?, ?, NULL)").run(mxid, roomID)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupTestSim(userID) {
|
|
||||||
const mxid = db.prepare("SELECT mxid FROM sim WHERE user_id = ?").pluck().get(userID)
|
|
||||||
if (mxid) {
|
|
||||||
db.prepare("DELETE FROM sim_member WHERE mxid = ?").run(mxid)
|
|
||||||
}
|
|
||||||
db.prepare("DELETE FROM sim WHERE user_id = ?").run(userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
test("remove-member: removeSimFromGuild removes sim from all guild rooms", async t => {
|
|
||||||
const mxid = "@_ooye_testrm1:cadence.moe"
|
|
||||||
const guildID = "112760669178241024"
|
|
||||||
const roomID1 = "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
|
||||||
const roomID2 = "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
|
||||||
|
|
||||||
setupTestSim("999999990", "testrm1", mxid)
|
|
||||||
setupTestSimMember(mxid, roomID1)
|
|
||||||
setupTestSimMember(mxid, roomID2)
|
|
||||||
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const count = await removeSimFromGuild(mxid, guildID)
|
|
||||||
|
|
||||||
t.equal(count, 2)
|
|
||||||
t.equal(leftRooms.length, 2)
|
|
||||||
t.ok(leftRooms.some(r => r.roomID === roomID1))
|
|
||||||
t.ok(leftRooms.some(r => r.roomID === roomID2))
|
|
||||||
|
|
||||||
const remaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid)
|
|
||||||
t.equal(remaining, 0)
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
cleanupTestSim("999999990")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: removeSimFromGuild only affects rooms in the specified guild", async t => {
|
|
||||||
const mxid = "@_ooye_testrm2:cadence.moe"
|
|
||||||
const guildID = "112760669178241024"
|
|
||||||
const guildRoom = "!kLRqKKUQXcibIMtOpl:cadence.moe" // guild 112760669178241024
|
|
||||||
const otherGuildRoom = "!BnKuBPCvyfOkhcUjEu:cadence.moe" // guild 66192955777486848
|
|
||||||
|
|
||||||
setupTestSim("999999991", "testrm2", mxid)
|
|
||||||
setupTestSimMember(mxid, guildRoom)
|
|
||||||
setupTestSimMember(mxid, otherGuildRoom)
|
|
||||||
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const count = await removeSimFromGuild(mxid, guildID)
|
|
||||||
|
|
||||||
t.equal(count, 1)
|
|
||||||
t.equal(leftRooms[0].roomID, guildRoom)
|
|
||||||
|
|
||||||
const otherRemaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ? AND room_id = ?").pluck().get(mxid, otherGuildRoom)
|
|
||||||
t.equal(otherRemaining, 1, "other guild's room should be untouched")
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
cleanupTestSim("999999991")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: removeSimFromGuild handles leaveRoom errors gracefully", async t => {
|
|
||||||
const mxid = "@_ooye_testrm3:cadence.moe"
|
|
||||||
const guildID = "112760669178241024"
|
|
||||||
const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
|
||||||
|
|
||||||
setupTestSim("999999992", "testrm3", mxid)
|
|
||||||
setupTestSimMember(mxid, roomID)
|
|
||||||
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async () => { throw new Error("not in room") }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const count = await removeSimFromGuild(mxid, guildID)
|
|
||||||
t.equal(count, 1)
|
|
||||||
|
|
||||||
const remaining = db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid)
|
|
||||||
t.equal(remaining, 0, "sim_member should be deleted even if leaveRoom fails")
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
cleanupTestSim("999999992")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: removeMember removes user sim and their PK proxy sims", async t => {
|
|
||||||
const userID = "999999993"
|
|
||||||
const mxid = "@_ooye_testrm4:cadence.moe"
|
|
||||||
const pkUserID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
|
||||||
const pkMxid = "@_ooye__pk_testrmpk:cadence.moe"
|
|
||||||
const guildID = "112760669178241024"
|
|
||||||
const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
|
||||||
|
|
||||||
setupTestSim(userID, "testrm4", mxid)
|
|
||||||
setupTestSimMember(mxid, roomID)
|
|
||||||
setupTestSim(pkUserID, "_pk_testrmpk", pkMxid)
|
|
||||||
setupTestSimMember(pkMxid, roomID)
|
|
||||||
db.prepare("INSERT OR IGNORE INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES (?, ?, ?)").run(pkUserID, userID, "Test PK")
|
|
||||||
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeMember(userID, guildID)
|
|
||||||
|
|
||||||
t.equal(leftRooms.length, 2)
|
|
||||||
t.ok(leftRooms.some(r => r.mxid === mxid), "user sim should leave")
|
|
||||||
t.ok(leftRooms.some(r => r.mxid === pkMxid), "pk sim should leave")
|
|
||||||
|
|
||||||
t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid), 0)
|
|
||||||
t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(pkMxid), 0)
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
db.prepare("DELETE FROM sim_proxy WHERE user_id = ?").run(pkUserID)
|
|
||||||
cleanupTestSim(userID)
|
|
||||||
cleanupTestSim(pkUserID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: removeMember does nothing for unknown user", async t => {
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeMember("000000000000000000", "112760669178241024")
|
|
||||||
t.equal(leftRooms.length, 0)
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: checkMissedMembers removes sims for departed users", async t => {
|
|
||||||
const userID = "999999994"
|
|
||||||
const mxid = "@_ooye_testrm5:cadence.moe"
|
|
||||||
const guildID = "112760669178241024"
|
|
||||||
const roomID = "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
|
||||||
|
|
||||||
setupTestSim(userID, "testrm5", mxid)
|
|
||||||
setupTestSimMember(mxid, roomID)
|
|
||||||
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
const originalSnow = discord.snow
|
|
||||||
discord.snow = {
|
|
||||||
guild: {
|
|
||||||
getGuildMember: async (gid, uid) => {
|
|
||||||
if (uid === userID) throw new Error('{"message": "Unknown Member", "code": 10007}')
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await checkMissedMembers({id: guildID})
|
|
||||||
|
|
||||||
t.ok(leftRooms.some(r => r.mxid === mxid), "departed user's sim should be removed")
|
|
||||||
t.equal(db.prepare("SELECT COUNT(*) FROM sim_member WHERE mxid = ?").pluck().get(mxid), 0)
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
discord.snow = originalSnow
|
|
||||||
cleanupTestSim(userID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test("remove-member: checkMissedMembers skips unavailable guilds", async t => {
|
|
||||||
const leftRooms = []
|
|
||||||
const originalLeaveRoom = api.leaveRoom
|
|
||||||
api.leaveRoom = async (roomID, userMxid) => { leftRooms.push({roomID, mxid: userMxid}) }
|
|
||||||
|
|
||||||
try {
|
|
||||||
await checkMissedMembers({id: "112760669178241024", unavailable: true})
|
|
||||||
t.equal(leftRooms.length, 0, "should not process unavailable guilds")
|
|
||||||
} finally {
|
|
||||||
api.leaveRoom = originalLeaveRoom
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -51,7 +51,7 @@ async function sendMessage(message, channel, guild, row) {
|
||||||
if (message.author.id === discord.application.id) {
|
if (message.author.id === discord.application.id) {
|
||||||
// no need to sync the bot's own user
|
// no need to sync the bot's own user
|
||||||
} else {
|
} else {
|
||||||
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID)
|
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID, message.interaction_metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert").strict
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {sync, select} = passthrough
|
const {sync, select} = passthrough
|
||||||
/** @type {import("../../matrix/api")} */
|
/** @type {import("../../matrix/api")} */
|
||||||
|
|
@ -26,7 +28,7 @@ const presenceLoopInterval = 28e3
|
||||||
|
|
||||||
// Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence
|
// Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence
|
||||||
const guildPresenceSetting = new class {
|
const guildPresenceSetting = new class {
|
||||||
/** @private @type {Set<string>} */ guilds
|
/** @private @type {Set<string>} */ guilds = new Set()
|
||||||
constructor() {
|
constructor() {
|
||||||
this.update()
|
this.update()
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +42,7 @@ const guildPresenceSetting = new class {
|
||||||
|
|
||||||
class Presence extends sync.reloadClassMethods(() => Presence) {
|
class Presence extends sync.reloadClassMethods(() => Presence) {
|
||||||
/** @type {string} */ userID
|
/** @type {string} */ userID
|
||||||
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string}} */ data
|
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string} | undefined} */ data
|
||||||
/** @private @type {?string | undefined} */ mxid
|
/** @private @type {?string | undefined} */ mxid
|
||||||
/** @private @type {number} */ delay = Math.random()
|
/** @private @type {number} */ delay = Math.random()
|
||||||
|
|
||||||
|
|
@ -66,6 +68,7 @@ class Presence extends sync.reloadClassMethods(() => Presence) {
|
||||||
// I haven't tried, but I assume Synapse explodes if you try to update too many presences at the same time.
|
// I haven't tried, but I assume Synapse explodes if you try to update too many presences at the same time.
|
||||||
// This random delay will space them out over the whole 28 second cycle.
|
// This random delay will space them out over the whole 28 second cycle.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
assert(this.data)
|
||||||
api.setPresence(this.data, mxid).catch(() => {})
|
api.setPresence(this.data, mxid).catch(() => {})
|
||||||
}, this.delay * presenceLoopInterval).unref()
|
}, this.delay * presenceLoopInterval).unref()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) {
|
||||||
/** @param {{id: string, type: "discordUser"}} node */
|
/** @param {{id: string, type: "discordUser"}} node */
|
||||||
user: node => {
|
user: node => {
|
||||||
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
||||||
const interaction = message.interaction_metadata || message.interaction
|
const interactionMetadata = message.interaction_metadata
|
||||||
const username = message.mentions?.find(ment => ment.id === node.id)?.username
|
const username = message.mentions?.find(ment => ment.id === node.id)?.username
|
||||||
|| message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
|
|| message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
|
||||||
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|
|| (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null)
|
||||||
|| (message.author?.id === node.id ? message.author.username : null)
|
|| (message.author?.id === node.id ? message.author.username : null)
|
||||||
|| "unknown-user"
|
|| "unknown-user"
|
||||||
if (mxid && useHTML) {
|
if (mxid && useHTML) {
|
||||||
|
|
@ -357,9 +357,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
const interaction = message.interaction_metadata || message.interaction
|
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
|
||||||
const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction
|
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||||
const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@type {{room?: boolean, user_ids?: string[]}}
|
@type {{room?: boolean, user_ids?: string[]}}
|
||||||
|
|
@ -400,6 +399,16 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
} else if (message.referenced_message) {
|
} else if (message.referenced_message) {
|
||||||
repliedToUnknownEvent = true
|
repliedToUnknownEvent = true
|
||||||
}
|
}
|
||||||
|
} else if (message.type === DiscordTypes.MessageType.ContextMenuCommand && message.interaction && message.message_reference?.message_id) {
|
||||||
|
// It could be a /plu/ral emulated reply
|
||||||
|
if (message.interaction.name.startsWith("Reply ") && message.content.startsWith("-# [↪](")) {
|
||||||
|
const row = await getHistoricalEventRow(message.message_reference?.message_id)
|
||||||
|
if (row && "event_id" in row) {
|
||||||
|
repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
|
||||||
|
message.content = message.content.replace(/^.*\n/, "")
|
||||||
|
isInteraction = false // declutter
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (dUtils.isWebhookMessage(message) && message.embeds[0]?.author?.name?.endsWith("↩️")) {
|
} else if (dUtils.isWebhookMessage(message) && message.embeds[0]?.author?.name?.endsWith("↩️")) {
|
||||||
// It could be a PluralKit emulated reply, let's see if it has a message link
|
// It could be a PluralKit emulated reply, let's see if it has a message link
|
||||||
const isEmulatedReplyToText = message.embeds[0].description?.startsWith("**[Reply to:]")
|
const isEmulatedReplyToText = message.embeds[0].description?.startsWith("**[Reply to:]")
|
||||||
|
|
@ -685,8 +694,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInteraction && !isThinkingInteraction && events.length === 0) {
|
if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) {
|
||||||
const formattedInteraction = getFormattedInteraction(interaction, false)
|
const formattedInteraction = getFormattedInteraction(message.interaction, false)
|
||||||
body = `${formattedInteraction.body}\n${body}`
|
body = `${formattedInteraction.body}\n${body}`
|
||||||
html = `${formattedInteraction.html}${html}`
|
html = `${formattedInteraction.html}${html}`
|
||||||
}
|
}
|
||||||
|
|
@ -782,8 +791,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
events.push(...forwardedEvents)
|
events.push(...forwardedEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isThinkingInteraction) {
|
if (isInteraction && isThinkingInteraction && message.interaction) {
|
||||||
const formattedInteraction = getFormattedInteraction(interaction, true)
|
const formattedInteraction = getFormattedInteraction(message.interaction, true)
|
||||||
await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice")
|
await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ test("message2event components: pk question mark output", async t => {
|
||||||
+ "<hr>"
|
+ "<hr>"
|
||||||
+ "<blockquote><p><strong>System:</strong> INX (<code>xffgnx</code>)"
|
+ "<blockquote><p><strong>System:</strong> INX (<code>xffgnx</code>)"
|
||||||
+ "<br><strong>Member:</strong> Lillith (<code>pphhoh</code>)"
|
+ "<br><strong>Member:</strong> Lillith (<code>pphhoh</code>)"
|
||||||
+ "<br><strong>Sent by:</strong> infinidoge1337 (@unknown-user:)"
|
+ "<br><strong>Sent by:</strong> infinidoge1337 (<a href=\"https://matrix.to/#/@_ooye_infinidoge1337:cadence.moe\">@unknown-user</a>)"
|
||||||
+ "<br><br><strong>Account Roles (7)</strong>"
|
+ "<br><br><strong>Account Roles (7)</strong>"
|
||||||
+ "<br>§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping</p>"
|
+ "<br>§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping</p>"
|
||||||
+ `🖼️ <a href="https://files.inx.moe/p/cdn/lillith.webp">https://files.inx.moe/p/cdn/lillith.webp</a>`
|
+ `🖼️ <a href="https://files.inx.moe/p/cdn/lillith.webp">https://files.inx.moe/p/cdn/lillith.webp</a>`
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,8 @@ test("message2event embeds: blockquote in embed", async t => {
|
||||||
t.equal(called, 1, "should call getJoinedMembers once")
|
t.equal(called, 1, "should call getJoinedMembers once")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("message2event embeds: crazy html is all escaped", async t => {
|
test("message2event embeds: extreme html is all escaped", async t => {
|
||||||
const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general)
|
const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general)
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
|
|
|
||||||
38
src/d2m/converters/remove-member-mxids.js
Normal file
38
src/d2m/converters/remove-member-mxids.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const passthrough = require("../../passthrough")
|
||||||
|
const {db, select, from} = passthrough
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} userID discord user ID that left
|
||||||
|
* @param {string} guildID discord guild ID that they left
|
||||||
|
*/
|
||||||
|
function removeMemberMxids(userID, guildID) {
|
||||||
|
// Get sims for user and remove
|
||||||
|
let membership = from("sim").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||||
|
.select("room_id", "mxid").where({user_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all()
|
||||||
|
membership = membership.concat(from("sim_proxy").join("sim", "user_id").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||||
|
.select("room_id", "mxid").where({proxy_owner_id: userID, guild_id: guildID}).and("ORDER BY room_id, mxid").all())
|
||||||
|
|
||||||
|
// Get user installed apps and remove
|
||||||
|
/** @type {string[]} */
|
||||||
|
let userAppDeletions = []
|
||||||
|
// 1. Select apps that have 1 user remaining
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
const appsWithOneUser = new Set(db.prepare("SELECT app_bot_id FROM app_user_install WHERE guild_id = ? GROUP BY app_bot_id HAVING count(*) = 1").pluck().all(guildID))
|
||||||
|
// 2. Select apps installed by this user
|
||||||
|
const appsFromThisUser = new Set(select("app_user_install", "app_bot_id", {guild_id: guildID, user_id: userID}).pluck().all())
|
||||||
|
if (appsFromThisUser.size) userAppDeletions.push(userID)
|
||||||
|
// Then remove user installed apps if this was the last user with them
|
||||||
|
const appsToRemove = appsWithOneUser.intersection(appsFromThisUser)
|
||||||
|
for (const botID of appsToRemove) {
|
||||||
|
// Remove sims for user installed app
|
||||||
|
const appRemoval = removeMemberMxids(botID, guildID)
|
||||||
|
membership = membership.concat(appRemoval.membership)
|
||||||
|
userAppDeletions = userAppDeletions.concat(appRemoval.userAppDeletions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {membership, userAppDeletions}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.removeMemberMxids = removeMemberMxids
|
||||||
43
src/d2m/converters/remove-member-mxids.test.js
Normal file
43
src/d2m/converters/remove-member-mxids.test.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const {removeMemberMxids} = require("./remove-member-mxids")
|
||||||
|
|
||||||
|
test("remove member mxids: would remove mxid for all rooms in this server", t => {
|
||||||
|
t.deepEqual(removeMemberMxids("772659086046658620", "112760669178241024"), {
|
||||||
|
userAppDeletions: [],
|
||||||
|
membership: [{
|
||||||
|
mxid: "@_ooye_cadence:cadence.moe",
|
||||||
|
room_id: "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||||
|
}, {
|
||||||
|
mxid: "@_ooye_cadence:cadence.moe",
|
||||||
|
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("remove member mxids: removes sims too", t => {
|
||||||
|
t.deepEqual(removeMemberMxids("196188877885538304", "112760669178241024"), {
|
||||||
|
userAppDeletions: [],
|
||||||
|
membership: [{
|
||||||
|
mxid: '@_ooye_ampflower:cadence.moe',
|
||||||
|
room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe'
|
||||||
|
}, {
|
||||||
|
mxid: '@_ooye__pk_zoego:cadence.moe',
|
||||||
|
room_id: '!qzDBLKlildpzrrOnFZ:cadence.moe'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("remove member mxids: removes apps too", t => {
|
||||||
|
t.deepEqual(removeMemberMxids("197126718400626689", "66192955777486848"), {
|
||||||
|
userAppDeletions: ["197126718400626689"],
|
||||||
|
membership: [{
|
||||||
|
mxid: '@_ooye_infinidoge1337:cadence.moe',
|
||||||
|
room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe'
|
||||||
|
}, {
|
||||||
|
mxid: '@_ooye_evil_lillith_sheher:cadence.moe',
|
||||||
|
room_id: '!BnKuBPCvyfOkhcUjEu:cadence.moe'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -23,7 +23,7 @@ class DiscordClient {
|
||||||
/** @type {import("cloudstorm").IClientOptions["intents"]} */
|
/** @type {import("cloudstorm").IClientOptions["intents"]} */
|
||||||
const intents = [
|
const intents = [
|
||||||
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
|
"DIRECT_MESSAGES", "DIRECT_MESSAGE_REACTIONS", "DIRECT_MESSAGE_TYPING",
|
||||||
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MEMBERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS",
|
"GUILDS", "GUILD_EMOJIS_AND_STICKERS", "GUILD_MESSAGES", "GUILD_MESSAGE_REACTIONS", "GUILD_MESSAGE_TYPING", "GUILD_WEBHOOKS", "GUILD_MESSAGE_POLLS",
|
||||||
"MESSAGE_CONTENT"
|
"MESSAGE_CONTENT"
|
||||||
]
|
]
|
||||||
if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES")
|
if (reg.ooye.receive_presences !== false) intents.push("GUILD_PRESENCES")
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,9 @@ const utils = {
|
||||||
if (listen === "full") {
|
if (listen === "full") {
|
||||||
try {
|
try {
|
||||||
await eventDispatcher.checkMissedExpressions(message.d)
|
await eventDispatcher.checkMissedExpressions(message.d)
|
||||||
await eventDispatcher.checkMissedPins(client, message.d)
|
|
||||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||||
await eventDispatcher.checkMissedMembers(message.d)
|
await eventDispatcher.checkMissedPins(client, message.d)
|
||||||
|
await eventDispatcher.checkMissedLeaves(client, message.d)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
|
||||||
|
|
@ -32,14 +32,14 @@ const speedbump = sync.require("./actions/speedbump")
|
||||||
const retrigger = sync.require("./actions/retrigger")
|
const retrigger = sync.require("./actions/retrigger")
|
||||||
/** @type {import("./actions/set-presence")} */
|
/** @type {import("./actions/set-presence")} */
|
||||||
const setPresence = sync.require("./actions/set-presence")
|
const setPresence = sync.require("./actions/set-presence")
|
||||||
|
/** @type {import("./actions/remove-member")} */
|
||||||
|
const removeMember = sync.require("./actions/remove-member")
|
||||||
/** @type {import("./actions/poll-vote")} */
|
/** @type {import("./actions/poll-vote")} */
|
||||||
const vote = sync.require("./actions/poll-vote")
|
const vote = sync.require("./actions/poll-vote")
|
||||||
/** @type {import("../m2d/event-dispatcher")} */
|
/** @type {import("../m2d/event-dispatcher")} */
|
||||||
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
const matrixEventDispatcher = sync.require("../m2d/event-dispatcher")
|
||||||
/** @type {import("../discord/interactions/matrix-info")} */
|
/** @type {import("../discord/interactions/matrix-info")} */
|
||||||
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
const matrixInfoInteraction = sync.require("../discord/interactions/matrix-info")
|
||||||
/** @type {import("./actions/remove-member")} */
|
|
||||||
const removeMember = sync.require("./actions/remove-member")
|
|
||||||
|
|
||||||
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
const {Semaphore} = require("@chriscdn/promise-semaphore")
|
||||||
const checkMissedPinsSema = new Semaphore()
|
const checkMissedPinsSema = new Semaphore()
|
||||||
|
|
@ -125,6 +125,7 @@ module.exports = {
|
||||||
// Send in order
|
// Send in order
|
||||||
for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) {
|
for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; i--) {
|
||||||
const message = messages[i]
|
const message = messages[i]
|
||||||
|
if (message.type === DiscordTypes.MessageType.UserJoin) continue // since join announcements don't become events, it would be a repetition to act on them during backfill
|
||||||
|
|
||||||
if (!members.has(message.author.id)) members.set(message.author.id, await client.snow.guild.getGuildMember(guild.id, message.author.id).catch(() => undefined))
|
if (!members.has(message.author.id)) members.set(message.author.id, await client.snow.guild.getGuildMember(guild.id, message.author.id).catch(() => undefined))
|
||||||
await module.exports.MESSAGE_CREATE(client, {
|
await module.exports.MESSAGE_CREATE(client, {
|
||||||
|
|
@ -174,6 +175,31 @@ module.exports = {
|
||||||
await createSpace.syncSpaceExpressions(data, true)
|
await createSpace.syncSpaceExpressions(data, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When logging back in, check if any members left while we were gone.
|
||||||
|
* Do this by getting the member list from Discord and seeing who we still have locally that isn't there in the response.
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {DiscordTypes.GatewayGuildCreateDispatchData} guild
|
||||||
|
*/
|
||||||
|
async checkMissedLeaves(client, guild) {
|
||||||
|
const maxLimit = 1000
|
||||||
|
if (guild.member_count >= maxLimit) return // too large to want to scan
|
||||||
|
const discordMembers = await client.snow.guild.getGuildMembers(guild.id, {limit: maxLimit})
|
||||||
|
if (discordMembers.length >= maxLimit) return // response was maxed out, there are guild members that weren't listed, can't act safely
|
||||||
|
const discordMembersSet = new Set(discordMembers.map(m => m.user.id))
|
||||||
|
// no indexes on this one but I'll cope
|
||||||
|
const membersAddedOnMatrix = new Set(from("sim").join("sim_member", "mxid").join("channel_room", "room_id")
|
||||||
|
.pluck("user_id").selectUnsafe("DISTINCT user_id").where({guild_id: guild.id}).and("AND user_id not like '%-%' and user_id not like '%\\_%' escape '\\'").all())
|
||||||
|
const userInstalledAppIDs = new Set(from("app_user_install").pluck("app_bot_id").selectUnsafe("DISTINCT app_bot_id").where({guild_id: guild.id}).all())
|
||||||
|
// loop over members added on matrix and if the member does not exist on discord-side then they should be removed
|
||||||
|
for (const userID of membersAddedOnMatrix) {
|
||||||
|
if (userInstalledAppIDs.has(userID)) continue // skip user installed apps here since they're never true members - they'll be removed by removeMember when the associated user is removed
|
||||||
|
if (!discordMembersSet.has(userID)) {
|
||||||
|
await removeMember.removeMember(userID, guild.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Announces to the parent room that the thread room has been created.
|
* Announces to the parent room that the thread room has been created.
|
||||||
* See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
|
* See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
|
||||||
|
|
@ -213,6 +239,14 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("./discord-client")} client
|
||||||
|
* @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data
|
||||||
|
*/
|
||||||
|
async GUILD_MEMBER_REMOVE(client, data) {
|
||||||
|
await removeMember.removeMember(data.user.id, data.guild_id)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
* @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread
|
* @param {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread
|
||||||
|
|
@ -415,24 +449,5 @@ module.exports = {
|
||||||
const status = data.status
|
const status = data.status
|
||||||
if (!status) return
|
if (!status) return
|
||||||
setPresence.presenceTracker.incomingPresence(data.user.id, data.guild_id, status)
|
setPresence.presenceTracker.incomingPresence(data.user.id, data.guild_id, status)
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a Discord user leaves (or is kicked/banned from) the guild, make their sims leave the bridged Matrix rooms.
|
|
||||||
* Also removes PluralKit proxy sims owned by the departing user.
|
|
||||||
* @param {import("./discord-client")} client
|
|
||||||
* @param {DiscordTypes.GatewayGuildMemberRemoveDispatchData} data
|
|
||||||
*/
|
|
||||||
async GUILD_MEMBER_REMOVE(client, data) {
|
|
||||||
if (data.user.id === client.user.id) return // Don't process if the bot itself is removed
|
|
||||||
await removeMember.removeMember(data.user.id, data.guild_id)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When logging back in, check for sims whose Discord users have left the guild while the bridge was offline.
|
|
||||||
* @param {DiscordTypes.GatewayGuildCreateDispatchData} guild
|
|
||||||
*/
|
|
||||||
async checkMissedMembers(guild) {
|
|
||||||
await removeMember.checkMissedMembers(guild)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ CREATE TABLE "role_default" (
|
||||||
"guild_id" TEXT NOT NULL,
|
"guild_id" TEXT NOT NULL,
|
||||||
"role_id" TEXT NOT NULL,
|
"role_id" TEXT NOT NULL,
|
||||||
PRIMARY KEY ("guild_id", "role_id")
|
PRIMARY KEY ("guild_id", "role_id")
|
||||||
);
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|
|
||||||
10
src/db/migrations/0036-app-user-install.sql
Normal file
10
src/db/migrations/0036-app-user-install.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE "app_user_install" (
|
||||||
|
"guild_id" TEXT NOT NULL,
|
||||||
|
"app_bot_id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY ("guild_id", "app_bot_id", "user_id")
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
6
src/db/orm-defs.d.ts
vendored
6
src/db/orm-defs.d.ts
vendored
|
|
@ -1,4 +1,10 @@
|
||||||
export type Models = {
|
export type Models = {
|
||||||
|
app_user_install: {
|
||||||
|
guild_id: string
|
||||||
|
app_bot_id: string
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
|
||||||
auto_emoji: {
|
auto_emoji: {
|
||||||
name: string
|
name: string
|
||||||
emoji_id: string
|
emoji_id: string
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
|
||||||
* @param {DiscordTypes.APIMessage} message
|
* @param {DiscordTypes.APIMessage} message
|
||||||
*/
|
*/
|
||||||
function isWebhookMessage(message) {
|
function isWebhookMessage(message) {
|
||||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
|
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -471,7 +471,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
||||||
// @ts-ignore - typescript doesn't know about indices yet
|
// @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]),
|
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `@everyone` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||||
ensureJoined: [],
|
ensureJoined: [],
|
||||||
allowedMentionsParse: ["everyone"]
|
allowedMentionsParse: ["everyone"],
|
||||||
|
allowedMentionsUsers: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (writtenMentionMatch[1].length < 40) { // the API supports up to 100 characters, but really if you're searching more than 40, something messed up
|
} else if (writtenMentionMatch[1].length < 40) { // the API supports up to 100 characters, but really if you're searching more than 40, something messed up
|
||||||
|
|
@ -482,7 +483,8 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
||||||
// @ts-ignore - typescript doesn't know about indices yet
|
// @ts-ignore - typescript doesn't know about indices yet
|
||||||
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]),
|
content: content.slice(0, writtenMentionMatch.indices[1][0]-1) + `<@${results[0].user.id}>` + content.slice(writtenMentionMatch.indices[1][1]),
|
||||||
ensureJoined: [results[0].user],
|
ensureJoined: [results[0].user],
|
||||||
allowedMentionsParse: []
|
allowedMentionsParse: [],
|
||||||
|
allowedMentionsUsers: [results[0].user.id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -544,6 +546,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
let displayName = event.sender
|
let displayName = event.sender
|
||||||
let avatarURL = undefined
|
let avatarURL = undefined
|
||||||
const allowedMentionsParse = ["users", "roles"]
|
const allowedMentionsParse = ["users", "roles"]
|
||||||
|
const allowedMentionsUsers = []
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
let messageIDsToEdit = []
|
let messageIDsToEdit = []
|
||||||
let replyLine = ""
|
let replyLine = ""
|
||||||
|
|
@ -763,7 +766,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
// Generate a reply preview for a standard message
|
// Generate a reply preview for a standard message
|
||||||
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
|
repliedToContent = repliedToContent.replace(/.*<\/mx-reply>/s, "") // Remove everything before replies, so just use the actual message body
|
||||||
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
repliedToContent = repliedToContent.replace(/^\s*<blockquote>.*?<\/blockquote>(.....)/s, "$1") // If the message starts with a blockquote, don't count it and use the message body afterwards
|
||||||
repliedToContent = repliedToContent.replace(/(?:\n|<br>)+/g, " ") // Should all be on one line
|
repliedToContent = repliedToContent.replace(/(?:\n|<br ?\/?>)+/g, " ") // Should all be on one line
|
||||||
repliedToContent = repliedToContent.replace(/<span [^>]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.)
|
repliedToContent = repliedToContent.replace(/<span [^>]*data-mx-spoiler\b[^>]*>.*?<\/span>/g, "[spoiler]") // Good enough method of removing spoiler content. (I don't want to break out the HTML parser unless I have to.)
|
||||||
repliedToContent = repliedToContent.replace(/<img([^>]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown
|
repliedToContent = repliedToContent.replace(/<img([^>]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown
|
||||||
const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/)
|
const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/)
|
||||||
|
|
@ -986,16 +989,34 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Complete content
|
||||||
content = displayNameRunoff + replyLine + content
|
content = displayNameRunoff + replyLine + content
|
||||||
|
|
||||||
// Split into 2000 character chunks
|
// Split into 2000 character chunks
|
||||||
const chunks = chunk(content, 2000)
|
const chunks = chunk(content, 2000)
|
||||||
|
|
||||||
|
// If m.mentions is specified and valid, overwrite allowedMentionsParse with a converted m.mentions
|
||||||
|
let allowed_mentions = {parse: allowedMentionsParse}
|
||||||
|
if (event.content["m.mentions"]) {
|
||||||
|
// Combine requested mentions with detected written mentions to get the full list
|
||||||
|
if (Array.isArray(event.content["m.mentions"].user_ids)) {
|
||||||
|
for (const mxid of event.content["m.mentions"].user_ids) {
|
||||||
|
const user_id = select("sim", "user_id", {mxid}).pluck().get()
|
||||||
|
if (!user_id) continue
|
||||||
|
allowedMentionsUsers.push(
|
||||||
|
select("sim_proxy", "proxy_owner_id", {user_id}).pluck().get() || user_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Specific mentions were requested, so do not parse users
|
||||||
|
allowed_mentions.parse = allowed_mentions.parse.filter(x => x !== "users")
|
||||||
|
allowed_mentions.users = allowedMentionsUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble chunks into Discord messages content
|
||||||
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
/** @type {(DiscordTypes.RESTPostAPIWebhookWithTokenJSONBody & {files?: {name: string, file: Buffer | stream.Readable}[]})[]} */
|
||||||
const messages = chunks.map(content => ({
|
const messages = chunks.map(content => ({
|
||||||
content,
|
content,
|
||||||
allowed_mentions: {
|
allowed_mentions,
|
||||||
parse: allowedMentionsParse
|
|
||||||
},
|
|
||||||
username: displayNameShortened,
|
username: displayNameShortened,
|
||||||
avatar_url: avatarURL
|
avatar_url: avatarURL
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,8 @@ test("event2message: markdown in link text does not attempt to be escaped becaus
|
||||||
content: "hey [@mario sports mix [she/her]](<https://matrix.to/#/%40cadence%3Acadence.moe>), is it possible to listen on a unix socket?",
|
content: "hey [@mario sports mix [she/her]](<https://matrix.to/#/%40cadence%3Acadence.moe>), is it possible to listen on a unix socket?",
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
parse: ["users", "roles"]
|
parse: ["roles"],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
@ -547,7 +548,8 @@ test("event2message: links don't have angle brackets added by accident", async t
|
||||||
content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp",
|
content: "Wanted to automate WG→AWG config enrichment and ended up basically coding a batch INI processor.\nhttps://github.com/Erquint/wgcbp",
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
parse: ["users", "roles"]
|
parse: ["roles"],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
@ -1296,7 +1298,8 @@ test("event2message: lists have appropriate line breaks", async t => {
|
||||||
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`,
|
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n* bridgeing specific channels with existing matrix rooms\n * optionally maybe entire "servers"\n* offering the bridge as a public service`,
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
parse: ["users", "roles"]
|
parse: ["roles"],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
@ -1337,7 +1340,8 @@ test("event2message: ordered list start attribute works", async t => {
|
||||||
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`,
|
content: `i am not certain what you mean by "already exists with as discord". my goals are\n\n1. bridgeing specific channels with existing matrix rooms\n 2. optionally maybe entire "servers"\n2. offering the bridge as a public service`,
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
parse: ["users", "roles"]
|
parse: ["roles"],
|
||||||
|
users: []
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
@ -1463,6 +1467,118 @@ test("event2message: rich reply to a sim user", async t => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("event2message: rich reply to a sim user, explicitly enabling mentions in client", async t => {
|
||||||
|
t.deepEqual(
|
||||||
|
await eventToMessage({
|
||||||
|
"type": "m.room.message",
|
||||||
|
"sender": "@cadence:cadence.moe",
|
||||||
|
"content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@_ooye_kyuugryphon:cadence.moe\">@_ooye_kyuugryphon:cadence.moe</a><br>Slow news day.</blockquote></mx-reply>Testing this reply, ignore",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"m.mentions": {
|
||||||
|
user_ids: ["@_ooye_kyuugryphon:cadence.moe"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1693029683016,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 91,
|
||||||
|
"transaction_id": "m1693029682894.510"
|
||||||
|
},
|
||||||
|
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
|
||||||
|
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||||
|
}, data.guild.general, data.channel.general, {
|
||||||
|
api: {
|
||||||
|
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Slow news day."
|
||||||
|
},
|
||||||
|
sender: "@_ooye_kyuugryphon:cadence.moe"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ensureJoined: [],
|
||||||
|
messagesToDelete: [],
|
||||||
|
messagesToEdit: [],
|
||||||
|
messagesToSend: [{
|
||||||
|
username: "cadence [they]",
|
||||||
|
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
|
||||||
|
+ " Slow news day."
|
||||||
|
+ "\nTesting this reply, ignore",
|
||||||
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["roles"],
|
||||||
|
users: ["111604486476181504"]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("event2message: rich reply to a sim user, explicitly disabling mentions in client", async t => {
|
||||||
|
t.deepEqual(
|
||||||
|
await eventToMessage({
|
||||||
|
"type": "m.room.message",
|
||||||
|
"sender": "@cadence:cadence.moe",
|
||||||
|
"content": {
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "> <@_ooye_kyuugryphon:cadence.moe> Slow news day.\n\nTesting this reply, ignore",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": "<mx-reply><blockquote><a href=\"https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04?via=cadence.moe&via=feather.onl\">In reply to</a> <a href=\"https://matrix.to/#/@_ooye_kyuugryphon:cadence.moe\">@_ooye_kyuugryphon:cadence.moe</a><br>Slow news day.</blockquote></mx-reply>Testing this reply, ignore",
|
||||||
|
"m.relates_to": {
|
||||||
|
"m.in_reply_to": {
|
||||||
|
"event_id": "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"m.mentions": {}
|
||||||
|
},
|
||||||
|
"origin_server_ts": 1693029683016,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 91,
|
||||||
|
"transaction_id": "m1693029682894.510"
|
||||||
|
},
|
||||||
|
"event_id": "$v_Gtr-bzv9IVlSLBO5DstzwmiDd-GSFaNfHX66IupV8",
|
||||||
|
"room_id": "!fGgIymcYWOqjbSRUdV:cadence.moe"
|
||||||
|
}, data.guild.general, data.channel.general, {
|
||||||
|
api: {
|
||||||
|
getEvent: mockGetEvent(t, "!fGgIymcYWOqjbSRUdV:cadence.moe", "$Fxy8SMoJuTduwReVkHZ1uHif9EuvNx36Hg79cltiA04", {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "Slow news day."
|
||||||
|
},
|
||||||
|
sender: "@_ooye_kyuugryphon:cadence.moe"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
ensureJoined: [],
|
||||||
|
messagesToDelete: [],
|
||||||
|
messagesToEdit: [],
|
||||||
|
messagesToSend: [{
|
||||||
|
username: "cadence [they]",
|
||||||
|
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1144865310588014633 <@111604486476181504>:"
|
||||||
|
+ " Slow news day."
|
||||||
|
+ "\nTesting this reply, ignore",
|
||||||
|
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||||
|
allowed_mentions: {
|
||||||
|
parse: ["roles"],
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => {
|
test("event2message: rich reply to a rich reply to a multi-line message should correctly strip reply fallback", async t => {
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
await eventToMessage({
|
await eventToMessage({
|
||||||
|
|
@ -1827,9 +1943,9 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
||||||
sender: "@rnl:cadence.moe",
|
sender: "@rnl:cadence.moe",
|
||||||
content: {
|
content: {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
body: `> <@cadence:cadence.moe> https://www.youtube.com/watch?v=uX32idb1jMw\n\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence:cadence.moe</a><br>https://www.youtube.com/watch?v=uX32idb1jMw</blockquote></mx-reply>Everyone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
formatted_body: `<mx-reply><blockquote><a href="https://matrix.to/#/!fGgIymcYWOqjbSRUdV:cadence.moe/$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU?via=cadence.moe">In reply to</a> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence:cadence.moe</a><br>https://www.youtube.com/watch?v=uX32idb1jMw</blockquote></mx-reply>Everyone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||||
"m.relates_to": {
|
"m.relates_to": {
|
||||||
"m.in_reply_to": {
|
"m.in_reply_to": {
|
||||||
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
|
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
|
||||||
|
|
@ -1859,7 +1975,7 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
||||||
username: "RNL",
|
username: "RNL",
|
||||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:"
|
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:"
|
||||||
+ " <https://www.youtube.com/watch?v=uX32idb1jMw>"
|
+ " <https://www.youtube.com/watch?v=uX32idb1jMw>"
|
||||||
+ `\nEveryone in the comments is like "you're so ahead of the curve" and "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
+ `\nEveryone in the comments is like "you're so ahead of the curve" and "can't believe this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||||
avatar_url: undefined,
|
avatar_url: undefined,
|
||||||
allowed_mentions: {
|
allowed_mentions: {
|
||||||
parse: ["users", "roles"]
|
parse: ["users", "roles"]
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,10 @@ async event => {
|
||||||
|
|
||||||
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
||||||
// Member is gone
|
// Member is gone
|
||||||
|
// if Matrix member, data was cached in member_cache
|
||||||
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)
|
||||||
|
// if Discord member (so kicked/banned by Matrix user), data was cached in sim_member
|
||||||
|
db.prepare("DELETE FROM sim_member WHERE room_id = ? and mxid = ?").run(event.room_id, event.state_key)
|
||||||
|
|
||||||
// Unregister room's use as a direct chat and/or an invite target 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) {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,12 @@ html(lang="en")
|
||||||
--_ts-multiple-bg: var(--green-400);
|
--_ts-multiple-bg: var(--green-400);
|
||||||
--_ts-multiple-fc: var(--white);
|
--_ts-multiple-fc: var(--white);
|
||||||
}
|
}
|
||||||
|
.s-avatar {
|
||||||
|
--_av-bg: var(--white);
|
||||||
|
}
|
||||||
|
.s-avatar .s-avatar--letter {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
.s-btn__dropdown:has(+ :popover-open) {
|
.s-btn__dropdown:has(+ :popover-open) {
|
||||||
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
|
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,13 @@ function tryStatic(event, fallthrough) {
|
||||||
// Everything else
|
// Everything else
|
||||||
else {
|
else {
|
||||||
const mime = mimeTypes.lookup(id)
|
const mime = mimeTypes.lookup(id)
|
||||||
if (typeof mime === "string") defaultContentType(event, mime)
|
if (typeof mime === "string") {
|
||||||
|
if (mime.startsWith("text/")) {
|
||||||
|
defaultContentType(event, mime + "; charset=utf-8") // usually wise
|
||||||
|
} else {
|
||||||
|
defaultContentType(event, mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
size: stats.size
|
size: stats.size
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +100,7 @@ function tryStatic(event, fallthrough) {
|
||||||
const path = join(publicDir, id)
|
const path = join(publicDir, id)
|
||||||
return pugSync.renderPath(event, path, {})
|
return pugSync.renderPath(event, path, {})
|
||||||
} else {
|
} else {
|
||||||
return fs.promises.readFile(join(publicDir, id))
|
return fs.createReadStream(join(publicDir, id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -4617,7 +4617,7 @@ module.exports = {
|
||||||
flags: 0,
|
flags: 0,
|
||||||
components: []
|
components: []
|
||||||
},
|
},
|
||||||
escaping_crazy_html_tags: {
|
extreme_html_escaping: {
|
||||||
id: "1158894131322552391",
|
id: "1158894131322552391",
|
||||||
type: 0,
|
type: 0,
|
||||||
content: "",
|
content: "",
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,28 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES
|
||||||
('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'),
|
('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'),
|
||||||
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
|
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
|
||||||
('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'),
|
('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian:cadence.moe'),
|
||||||
('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe');
|
('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe'),
|
||||||
|
('196188877885538304', 'ampflower', 'ampflower', '@_ooye_ampflower:cadence.moe'),
|
||||||
|
('1458668878107381800', 'Evil Lillith (she/her)', 'evil_lillith_sheher', '@_ooye_evil_lillith_sheher:cadence.moe'),
|
||||||
|
('197126718400626689', 'infinidoge1337', 'infinidoge1337', '@_ooye_infinidoge1337:cadence.moe');
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
|
INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
|
||||||
('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL),
|
('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC:cadence.moe', NULL),
|
||||||
('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
|
('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
|
||||||
|
('@_ooye_cadence:cadence.moe', '!kLRqKKUQXcibIMtOpl:cadence.moe', NULL),
|
||||||
|
('@_ooye_cadence:cadence.moe', '!fGgIymcYWOqjbSRUdV:cadence.moe', NULL),
|
||||||
|
('@_ooye_ampflower:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
|
||||||
|
('@_ooye__pk_zoego:cadence.moe', '!qzDBLKlildpzrrOnFZ:cadence.moe', NULL),
|
||||||
|
('@_ooye_infinidoge1337:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL),
|
||||||
|
('@_ooye_evil_lillith_sheher:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
|
||||||
|
|
||||||
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
|
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
|
||||||
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
|
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
|
||||||
|
|
||||||
|
INSERT INTO app_user_install (guild_id, app_bot_id, user_id) VALUES
|
||||||
|
('66192955777486848', '1458668878107381800', '197126718400626689');
|
||||||
|
|
||||||
INSERT INTO message_room (message_id, historical_room_index)
|
INSERT INTO message_room (message_id, historical_room_index)
|
||||||
WITH a (message_id, channel_id) AS (VALUES
|
WITH a (message_id, channel_id) AS (VALUES
|
||||||
('1106366167788044450', '122155380120748034'),
|
('1106366167788044450', '122155380120748034'),
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/d2m/actions/create-room.test")
|
require("../src/d2m/actions/create-room.test")
|
||||||
require("../src/d2m/actions/create-space.test")
|
require("../src/d2m/actions/create-space.test")
|
||||||
require("../src/d2m/actions/register-user.test")
|
require("../src/d2m/actions/register-user.test")
|
||||||
require("../src/d2m/actions/remove-member.test")
|
|
||||||
require("../src/d2m/converters/edit-to-changes.test")
|
require("../src/d2m/converters/edit-to-changes.test")
|
||||||
require("../src/d2m/converters/emoji-to-key.test")
|
require("../src/d2m/converters/emoji-to-key.test")
|
||||||
require("../src/d2m/converters/find-mentions.test")
|
require("../src/d2m/converters/find-mentions.test")
|
||||||
|
|
@ -153,6 +152,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/d2m/converters/message-to-event.test.embeds")
|
require("../src/d2m/converters/message-to-event.test.embeds")
|
||||||
require("../src/d2m/converters/message-to-event.test.pk")
|
require("../src/d2m/converters/message-to-event.test.pk")
|
||||||
require("../src/d2m/converters/pins-to-list.test")
|
require("../src/d2m/converters/pins-to-list.test")
|
||||||
|
require("../src/d2m/converters/remove-member-mxids.test")
|
||||||
require("../src/d2m/converters/remove-reaction.test")
|
require("../src/d2m/converters/remove-reaction.test")
|
||||||
require("../src/d2m/converters/thread-to-announcement.test")
|
require("../src/d2m/converters/thread-to-announcement.test")
|
||||||
require("../src/d2m/converters/user-to-mxid.test")
|
require("../src/d2m/converters/user-to-mxid.test")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue