Compare commits
No commits in common. "main" and "main" have entirely different histories.
68 changed files with 937 additions and 2613 deletions
|
|
@ -89,14 +89,15 @@ Whether you read those or not, I'm more than happy to help you 1-on-1 with codin
|
|||
|
||||
# Dependency justification
|
||||
|
||||
Total transitive production dependencies: 144
|
||||
Total transitive production dependencies: 134
|
||||
|
||||
### <font size="+2">🦕</font>
|
||||
|
||||
* (35) better-sqlite3: SQLite is the best database, and this is the best library for it.
|
||||
* (29) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets. It has libvips prebuilts for each platform.
|
||||
* (26) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
||||
* (9) h3: Web server. OOYE needs this for the web UI, appservice listener, authmedia proxy, and more.
|
||||
* (31) better-sqlite3: SQLite is the best database, and this is the best library for it.
|
||||
* (27) @cloudrac3r/pug: Language for dynamic web pages. This is my fork. (I released code that hadn't made it to npm, and removed the heavy pug-filters feature.)
|
||||
* (16) stream-mime-type@1: This seems like the best option. Version 1 is used because version 2 is ESM-only.
|
||||
* (9) h3: Web server. OOYE needs this for the appservice listener, authmedia proxy, self-service, and more.
|
||||
* (11) sharp: Image resizing and compositing. OOYE needs this for the emoji sprite sheets.
|
||||
|
||||
### <font size="-1">🪱</font>
|
||||
|
||||
|
|
@ -107,7 +108,6 @@ Total transitive production dependencies: 144
|
|||
* (0) @cloudrac3r/in-your-element: This is my Matrix Appservice API library. It depends on h3 and zod, which are already pulled in by OOYE.
|
||||
* (0) @cloudrac3r/mixin-deep: This is my fork. (It fixes a bug in regular mixin-deep.)
|
||||
* (0) @cloudrac3r/pngjs: Lottie stickers are converted to bitmaps with the vendored Rlottie WASM build, then the bitmaps are converted to PNG with pngjs.
|
||||
* (0) @cloudrac3r/stream-type: Determine type of Matrix files that don't specify it in info. Switched from stream-mime-type to this.
|
||||
* (0) @cloudrac3r/turndown: This HTML-to-Markdown converter looked the most suitable. I forked it to change the escaping logic to match the way Discord works.
|
||||
* (3) @stackoverflow/stacks: Stack Overflow design language and icons.
|
||||
* (0) ansi-colors: Helps with interactive prompting for the initial setup, and it's already pulled in by enquirer.
|
||||
|
|
@ -115,12 +115,12 @@ Total transitive production dependencies: 144
|
|||
* (0) cloudstorm: Discord gateway library with bring-your-own-caching that I trust.
|
||||
* (0) discord-api-types: Bitfields needed at runtime and types needed for development.
|
||||
* (0) domino: DOM implementation that's already pulled in by turndown.
|
||||
* (2) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||
* (1) enquirer: Interactive prompting for the initial setup rather than forcing users to edit YAML non-interactively.
|
||||
* (0) entities: Looks fine. No dependencies.
|
||||
* (0) get-relative-path: Looks fine. No dependencies.
|
||||
* (1) heatsync: Module hot-reloader that I trust.
|
||||
* (0) lru-cache: For holding unused nonce in memory and letting them be overwritten later if never used.
|
||||
* (1) mime-types: List of mime type mappings. Needed to serve static files.
|
||||
* (0) mime-type: File extension to mime type mapping that's already pulled in by stream-mime-type.
|
||||
* (0) prettier-bytes: It does what I want and has no dependencies.
|
||||
* (0) snowtransfer: Discord API library with bring-your-own-caching that I trust.
|
||||
* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well.
|
||||
|
|
|
|||
1117
package-lock.json
generated
1117
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "out-of-your-element",
|
||||
"version": "3.5.1",
|
||||
"version": "3.4.0",
|
||||
"description": "A bridge between Matrix and Discord",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
|
|
@ -19,35 +19,35 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||
"@cloudrac3r/discord-markdown": "^2.7.0",
|
||||
"@cloudrac3r/discord-markdown": "^2.6.10",
|
||||
"@cloudrac3r/giframe": "^0.4.3",
|
||||
"@cloudrac3r/html-template-tag": "^5.0.1",
|
||||
"@cloudrac3r/in-your-element": "^1.1.1",
|
||||
"@cloudrac3r/mixin-deep": "^3.0.1",
|
||||
"@cloudrac3r/pngjs": "^7.0.3",
|
||||
"@cloudrac3r/pug": "^4.0.4",
|
||||
"@cloudrac3r/stream-type": "^1.0.0",
|
||||
"@cloudrac3r/turndown": "^7.1.4",
|
||||
"@stackoverflow/stacks": "^2.5.4",
|
||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||
"ansi-colors": "^4.1.3",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"chunk-text": "^2.0.1",
|
||||
"cloudstorm": "^0.17.0",
|
||||
"cloudstorm": "^0.15.2",
|
||||
"discord-api-types": "^0.38.38",
|
||||
"domino": "^2.1.6",
|
||||
"enquirer": "^2.4.1",
|
||||
"entities": "^5.0.0",
|
||||
"get-relative-path": "^1.0.2",
|
||||
"h3": "^1.15.10",
|
||||
"h3": "^1.15.1",
|
||||
"heatsync": "^2.7.2",
|
||||
"htmx.org": "^2.0.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"prettier-bytes": "^1.0.4",
|
||||
"sharp": "^0.34.5",
|
||||
"snowtransfer": "^0.17.5",
|
||||
"try-to-catch": "^4.0.5",
|
||||
"snowtransfer": "^0.17.1",
|
||||
"stream-mime-type": "^1.0.2",
|
||||
"try-to-catch": "^3.0.1",
|
||||
"uqr": "^0.1.2",
|
||||
"xxhash-wasm": "^1.0.2",
|
||||
"zod": "^4.0.17"
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
"devDependencies": {
|
||||
"@cloudrac3r/tap-dot": "^2.0.3",
|
||||
"@types/node": "^22.17.1",
|
||||
"c8": "^11.0.0",
|
||||
"c8": "^10.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"supertape": "^12.0.12"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ if (!channelID) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
const assert = require("assert/strict")
|
||||
const sqlite = require("better-sqlite3")
|
||||
const backfill = new sqlite("scripts/backfill.db")
|
||||
backfill.prepare("CREATE TABLE IF NOT EXISTS backfill (channel_id TEXT NOT NULL, message_id INTEGER NOT NULL, PRIMARY KEY (channel_id, message_id))").run()
|
||||
|
|
@ -37,8 +38,12 @@ passthrough.select = orm.select
|
|||
|
||||
/** @type {import("../src/d2m/event-dispatcher")}*/
|
||||
const eventDispatcher = sync.require("../src/d2m/event-dispatcher")
|
||||
/** @type {import("../src/d2m/actions/create-room")} */
|
||||
const createRoom = sync.require("../src/d2m/actions/create-room")
|
||||
|
||||
const roomID = passthrough.select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
||||
if (!roomID) {
|
||||
console.error("Please choose a channel that's already bridged.")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
await discord.cloud.connect()
|
||||
|
|
@ -55,29 +60,23 @@ async function event(event) {
|
|||
if (!channel) return
|
||||
const guild_id = event.d.id
|
||||
|
||||
try {
|
||||
await createRoom.syncRoom(channelID)
|
||||
let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0"
|
||||
console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`)
|
||||
let last = backfill.prepare("SELECT cast(max(message_id) as TEXT) FROM backfill WHERE channel_id = ?").pluck().get(channelID) || "0"
|
||||
console.log(`OK, processing messages for #${channel.name}, continuing from ${last}`)
|
||||
|
||||
while (last) {
|
||||
const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)})
|
||||
messages.reverse() // More recent messages come first -> More recent messages come last
|
||||
for (const message of messages) {
|
||||
const simulatedGatewayDispatchData = {
|
||||
guild_id,
|
||||
backfill: true,
|
||||
...message
|
||||
}
|
||||
await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData)
|
||||
preparedInsert.run(channelID, message.id)
|
||||
while (last) {
|
||||
const messages = await discord.snow.channel.getChannelMessages(channelID, {limit: 50, after: String(last)})
|
||||
messages.reverse() // More recent messages come first -> More recent messages come last
|
||||
for (const message of messages) {
|
||||
const simulatedGatewayDispatchData = {
|
||||
guild_id,
|
||||
backfill: true,
|
||||
...message
|
||||
}
|
||||
last = messages.at(-1)?.id
|
||||
await eventDispatcher.MESSAGE_CREATE(discord, simulatedGatewayDispatchData)
|
||||
preparedInsert.run(channelID, message.id)
|
||||
}
|
||||
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1) // won't exit automatically on thrown error due to living discord connection, so manual exit is necessary
|
||||
last = messages.at(-1)?.id
|
||||
}
|
||||
|
||||
process.exit()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
// @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
|
||||
}
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
|
@ -13,5 +13,5 @@ const {prompt} = require("enquirer")
|
|||
|
||||
reg.ooye.web_password = passwordResponse.web_password
|
||||
writeRegistration(reg)
|
||||
console.log("Saved. This change should be applied instantly.")
|
||||
console.log("Saved. Restart Out Of Your Element to apply this change.")
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ async function channelToKState(channel, guild, di) {
|
|||
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
||||
}
|
||||
|
||||
const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
const everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
||||
const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)
|
||||
|
||||
|
|
@ -193,16 +193,6 @@ async function channelToKState(channel, guild, di) {
|
|||
// Don't overwrite room topic if the topic has been customised
|
||||
if (hasCustomTopic) delete channelKState["m.room.topic/"]
|
||||
|
||||
// Make voice channels be a Matrix voice room (MSC3417)
|
||||
if (channel.type === DiscordTypes.ChannelType.GuildVoice) {
|
||||
creationContent.type = "org.matrix.msc3417.call"
|
||||
channelKState["org.matrix.msc3401.call/"] = {
|
||||
"m.intent": "m.room",
|
||||
"m.type": "m.voice",
|
||||
"m.name": customName || channel.name
|
||||
}
|
||||
}
|
||||
|
||||
// Don't add a space parent if it's self service
|
||||
// (The person setting up self-service has already put it in their preferred space to be able to get this far.)
|
||||
const autocreate = select("guild_active", "autocreate", {guild_id: guild.id}).pluck().get()
|
||||
|
|
@ -266,7 +256,7 @@ async function createRoom(channel, guild, spaceID, kstate, privacyLevel) {
|
|||
|
||||
/**
|
||||
* Handling power levels separately. The spec doesn't specify what happens, Dendrite differs,
|
||||
* and Synapse does a very poorly thought out *shallow merge* of what I provide on top of what it creates.
|
||||
* and Synapse does an absolutely insane *shallow merge* of what I provide on top of what it creates.
|
||||
* 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/matrix-spec/issues/492
|
||||
|
|
@ -452,9 +442,8 @@ function syncRoom(channelID) {
|
|||
/**
|
||||
* @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
|
||||
* @param {string} guildID
|
||||
* @param {string} messageBeforeLeave
|
||||
*/
|
||||
async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room was removed from the bridge.") {
|
||||
async function unbridgeChannel(channel, guildID) {
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
assert.ok(roomID)
|
||||
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
|
||||
|
|
@ -504,7 +493,7 @@ async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room
|
|||
// send a notification in the room
|
||||
await api.sendEvent(roomID, "m.room.message", {
|
||||
msgtype: "m.notice",
|
||||
body: `⚠️ ${messageBeforeLeave}`
|
||||
body: "⚠️ This room was removed from the bridge."
|
||||
})
|
||||
|
||||
// if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged
|
||||
|
|
|
|||
|
|
@ -190,17 +190,6 @@ test("channel2room: read-only discord channel", async t => {
|
|||
t.equal(api.getCalled(), 2)
|
||||
})
|
||||
|
||||
test("channel2room: voice channel", async t => {
|
||||
const api = mockAPI(t)
|
||||
const state = kstateStripConditionals(await channelToKState(testData.channel.voice, testData.guild.general, {api}).then(x => x.channelKState))
|
||||
t.equal(state["m.room.create/"].type, "org.matrix.msc3417.call")
|
||||
t.deepEqual(state["org.matrix.msc3401.call/"], {
|
||||
"m.intent": "m.room",
|
||||
"m.name": "🍞丨[8user] Piece",
|
||||
"m.type": "m.voice"
|
||||
})
|
||||
})
|
||||
|
||||
test("convertNameAndTopic: custom name and topic", t => {
|
||||
t.deepEqual(
|
||||
_convertNameAndTopic({id: "123", name: "the-twilight-zone", topic: "Spooky stuff here. :ghost:", type: 0}, {id: "456"}, "hauntings"),
|
||||
|
|
|
|||
|
|
@ -34,10 +34,7 @@ async function emojisToState(emojis, guild) {
|
|||
if (e.data?.errcode === "M_TOO_LARGE") { // Very unlikely to happen. Only possible for 3x-series emojis uploaded shortly after animated emojis were introduced, when there was no 256 KB size limit.
|
||||
return
|
||||
}
|
||||
e["emoji"] = {
|
||||
name: emoji.name,
|
||||
id: emoji.id
|
||||
}
|
||||
console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`)
|
||||
throw e
|
||||
})
|
||||
))
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ function memberToPowerLevel(user, member, guild, channel) {
|
|||
if (!member) return 0
|
||||
|
||||
const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, user.id, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const everyonePermissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
/*
|
||||
* PL 100 = Administrator = People who can brick the room. RATIONALE:
|
||||
* - Administrator.
|
||||
|
|
@ -206,16 +206,14 @@ function _hashProfileContent(content, powerLevel) {
|
|||
* 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
|
||||
* 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 {Omit<DiscordTypes.APIGuildMember, "user"> | undefined} member
|
||||
* @param {DiscordTypes.APIGuildChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {string} roomID
|
||||
* @param {DiscordTypes.APIMessageInteractionMetadata} [interactionMetadata]
|
||||
* @returns {Promise<string>} mxid of the updated sim
|
||||
*/
|
||||
async function syncUser(user, member, channel, guild, roomID, interactionMetadata) {
|
||||
async function syncUser(user, member, channel, guild, roomID) {
|
||||
const mxid = await ensureSimJoined(user, roomID)
|
||||
const content = await memberToStateContent(user, member, guild.id)
|
||||
const powerLevel = memberToPowerLevel(user, member, guild, channel)
|
||||
|
|
@ -224,12 +222,6 @@ async function syncUser(user, member, channel, guild, roomID, interactionMetadat
|
|||
allowOverwrite: !!member,
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {sync, db, select, from} = passthrough
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../converters/remove-member-mxids")} */
|
||||
const removeMemberMxids = sync.require("../converters/remove-member-mxids")
|
||||
|
||||
/**
|
||||
* @param {string} userID discord user ID that left
|
||||
* @param {string} guildID discord guild ID that they left
|
||||
*/
|
||||
async function removeMember(userID, guildID) {
|
||||
const {userAppDeletions, membership} = removeMemberMxids.removeMemberMxids(userID, guildID)
|
||||
db.transaction(() => {
|
||||
for (const d of userAppDeletions) {
|
||||
db.prepare("DELETE FROM app_user_install WHERE guild_id = ? and user_id = ?").run(guildID, d)
|
||||
}
|
||||
})()
|
||||
for (const m of membership) {
|
||||
try {
|
||||
await api.leaveRoom(m.room_id, m.mxid)
|
||||
} catch (e) {
|
||||
if (String(e).includes("not in room")) {
|
||||
// no further action needed
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.removeMember = removeMember
|
||||
|
|
@ -51,7 +51,7 @@ async function sendMessage(message, channel, guild, row) {
|
|||
if (message.author.id === discord.application.id) {
|
||||
// no need to sync the bot's own user
|
||||
} else {
|
||||
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID, message.interaction_metadata)
|
||||
senderMxid = await registerUser.syncUser(message.author, message.member, channel, guild, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
|
||||
const passthrough = require("../../passthrough")
|
||||
const {sync, select} = passthrough
|
||||
/** @type {import("../../matrix/api")} */
|
||||
|
|
@ -28,7 +26,7 @@ const presenceLoopInterval = 28e3
|
|||
|
||||
// Cache the list of enabled guilds rather than accessing it like multiple times per second when any user changes presence
|
||||
const guildPresenceSetting = new class {
|
||||
/** @private @type {Set<string>} */ guilds = new Set()
|
||||
/** @private @type {Set<string>} */ guilds
|
||||
constructor() {
|
||||
this.update()
|
||||
}
|
||||
|
|
@ -42,7 +40,7 @@ const guildPresenceSetting = new class {
|
|||
|
||||
class Presence extends sync.reloadClassMethods(() => Presence) {
|
||||
/** @type {string} */ userID
|
||||
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string} | undefined} */ data
|
||||
/** @type {{presence: "online" | "offline" | "unavailable", status_msg?: string}} */ data
|
||||
/** @private @type {?string | undefined} */ mxid
|
||||
/** @private @type {number} */ delay = Math.random()
|
||||
|
||||
|
|
@ -68,7 +66,6 @@ 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.
|
||||
// This random delay will space them out over the whole 28 second cycle.
|
||||
setTimeout(() => {
|
||||
assert(this.data)
|
||||
api.setPresence(this.data, mxid).catch(() => {})
|
||||
}, this.delay * presenceLoopInterval).unref()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,11 +151,9 @@ async function editToChanges(message, guild, api) {
|
|||
const messageReallyOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 2 * 60 * 1000 // older than 2 minutes ago
|
||||
// Don't post new generated embeds for messages if the setting was disabled.
|
||||
const embedsEnabled = select("guild_space", "url_preview", {guild_id: guild?.id}).pluck().get() ?? 1
|
||||
// Bots may rely on embeds to send new content, so the rules may be more lax for them.
|
||||
const botEmbedsApproved = message.author?.bot && !originallyFromMatrix
|
||||
if (messageReallyOld) {
|
||||
eventsToSend = [] // Only allow edits to change and delete, but not send new.
|
||||
} else if ((messageQuiteOld || !embedsEnabled) && !botEmbedsApproved) {
|
||||
} else if ((messageQuiteOld || !embedsEnabled) && !message.author?.bot) {
|
||||
eventsToSend = eventsToSend.filter(e => e.msgtype !== "m.notice") // Only send events that aren't embeds.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ test("edit2changes: bot response", async t => {
|
|||
newContent: {
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "* :ae_botrac4r: @cadence asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
body: "* :ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '* <img data-mx-emoticon height="32" src="mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs" title=":ae_botrac4r:" alt=":ae_botrac4r:"> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence</a> asked <code></code>, I respond: Stop drinking paint. (No)<br><br>Hit <img data-mx-emoticon height="32" src="mxc://cadence.moe/OIpqpfxTnHKokcsYqDusxkBT" title=":bn_re:" alt=":bn_re:"> to reroll.',
|
||||
"m.mentions": {
|
||||
|
|
@ -87,7 +87,7 @@ test("edit2changes: bot response", async t => {
|
|||
// *** Replaced With: ***
|
||||
"m.new_content": {
|
||||
msgtype: "m.text",
|
||||
body: ":ae_botrac4r: @cadence asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
body: ":ae_botrac4r: [@cadence](https://matrix.to/#/@cadence:cadence.moe) asked ````, I respond: Stop drinking paint. (No)\n\nHit :bn_re: to reroll.",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: '<img data-mx-emoticon height="32" src="mxc://cadence.moe/skqfuItqxNmBYekzmVKyoLzs" title=":ae_botrac4r:" alt=":ae_botrac4r:"> <a href="https://matrix.to/#/@cadence:cadence.moe">@cadence</a> asked <code></code>, I respond: Stop drinking paint. (No)<br><br>Hit <img data-mx-emoticon height="32" src="mxc://cadence.moe/OIpqpfxTnHKokcsYqDusxkBT" title=":bn_re:" alt=":bn_re:"> to reroll.',
|
||||
"m.mentions": {
|
||||
|
|
|
|||
|
|
@ -146,18 +146,10 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
|||
// Highlight the relevant part of the message
|
||||
const start = baseOffset + best.scored.matchedInputTokens[0].index
|
||||
const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end
|
||||
const newNodes = [{
|
||||
type: "text", content: content.slice(0, start)
|
||||
}, {
|
||||
type: "link", target: `https://matrix.to/#/${best.mxid}`, content: [
|
||||
{type: "text", content: content.slice(start, end)}
|
||||
]
|
||||
}, {
|
||||
type: "text", content: content.slice(end)
|
||||
}]
|
||||
const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end)
|
||||
return {
|
||||
mxid: best.mxid,
|
||||
newNodes
|
||||
newContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) {
|
|||
/** @param {{id: string, type: "discordUser"}} node */
|
||||
user: node => {
|
||||
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
|
||||
const interactionMetadata = message.interaction_metadata
|
||||
const interaction = message.interaction_metadata || message.interaction
|
||||
const username = message.mentions?.find(ment => ment.id === node.id)?.username
|
||||
|| message.referenced_message?.mentions?.find(ment => ment.id === node.id)?.username
|
||||
|| (interactionMetadata?.user.id === node.id ? interactionMetadata.user.username : null)
|
||||
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|
||||
|| (message.author?.id === node.id ? message.author.username : null)
|
||||
|| "unknown-user"
|
||||
if (mxid && useHTML) {
|
||||
|
|
@ -261,29 +261,6 @@ function getFormattedInteraction(interaction, isThinkingInteraction) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} newEvents merge into events
|
||||
* @param {any} events will be modified
|
||||
* @param {boolean} forceSameMsgtype whether m.text may only be combined with m.text, etc
|
||||
*/
|
||||
function mergeTextEvents(newEvents, events, forceSameMsgtype) {
|
||||
let prev = events.at(-1)
|
||||
for (const ne of newEvents) {
|
||||
const isAllText = prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(ne.msgtype) && ["m.text", "m.notice"].includes(prev?.msgtype)
|
||||
const typesPermitted = !forceSameMsgtype || ne?.msgtype === prev?.msgtype
|
||||
if (isAllText && typesPermitted) {
|
||||
const rep = new mxUtils.MatrixStringBuilder()
|
||||
rep.body = prev.body
|
||||
rep.formattedBody = prev.formatted_body
|
||||
rep.addLine(ne.body, ne.formatted_body)
|
||||
prev.body = rep.body
|
||||
prev.formatted_body = rep.formattedBody
|
||||
} else {
|
||||
events.push(ne)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessage} message
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
|
|
@ -357,19 +334,9 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}]
|
||||
}
|
||||
|
||||
if (message.type === DiscordTypes.MessageType.ChannelFollowAdd) {
|
||||
return [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.emote",
|
||||
body: `set this room to receive announcements from ${message.content}`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: tag`set this room to receive announcements from <strong>${message.content}</strong>`,
|
||||
"m.mentions": {}
|
||||
}]
|
||||
}
|
||||
|
||||
let isInteraction = (message.type === DiscordTypes.MessageType.ChatInputCommand || message.type === DiscordTypes.MessageType.ContextMenuCommand) && message.interaction && "name" in message.interaction
|
||||
let isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||
const interaction = message.interaction_metadata || message.interaction
|
||||
const isInteraction = message.type === DiscordTypes.MessageType.ChatInputCommand && !!interaction && "name" in interaction
|
||||
const isThinkingInteraction = isInteraction && !!((message.flags || 0) & DiscordTypes.MessageFlags.Loading)
|
||||
|
||||
/**
|
||||
@type {{room?: boolean, user_ids?: string[]}}
|
||||
|
|
@ -410,16 +377,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
} else if (message.referenced_message) {
|
||||
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("↩️")) {
|
||||
// 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:]")
|
||||
|
|
@ -562,60 +519,29 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed
|
||||
}))
|
||||
|
||||
async function transformParsedVia(parsed, scanTextForMentions) {
|
||||
for (let n = 0; n < parsed.length; n++) {
|
||||
const node = parsed[n]
|
||||
async function transformParsedVia(parsed) {
|
||||
for (const node of parsed) {
|
||||
if (node.type === "discordChannel" || node.type === "discordChannelLink") {
|
||||
node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get()
|
||||
if (node.row?.room_id) {
|
||||
node.via = await getViaServersMemo(node.row.room_id)
|
||||
}
|
||||
}
|
||||
else if (node.type === "text" && typeof node.content === "string") {
|
||||
// Merge adjacent text nodes into this one
|
||||
while (parsed[n+1]?.type === "text" && typeof parsed[n+1].content === "string") {
|
||||
node.content += parsed[n+1].content
|
||||
parsed.splice(n+1, 1)
|
||||
}
|
||||
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
||||
if (scanTextForMentions) {
|
||||
let content = node.content
|
||||
const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)]
|
||||
for (let i = matches.length; i--;) {
|
||||
const m = matches[i]
|
||||
const prefix = m[1]
|
||||
const maximumWrittenSection = m[2].toLowerCase()
|
||||
if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it
|
||||
if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here
|
||||
|
||||
var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||
assert(roomID)
|
||||
var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name})))
|
||||
|
||||
const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content)
|
||||
if (found) {
|
||||
addMention(found.mxid)
|
||||
parsed.splice(n, 1, ...found.newNodes)
|
||||
content = found.newNodes[0].content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const maybeChildNodesArray of [node, node.content, node.items]) {
|
||||
if (Array.isArray(maybeChildNodesArray)) {
|
||||
await transformParsedVia(maybeChildNodesArray, scanTextForMentions && ["blockQuote", "list", "paragraph", "em", "strong", "u", "del", "text"].includes(node.type))
|
||||
await transformParsedVia(maybeChildNodesArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
let html = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, customOptions.isTheMessageContent && options.scanTextForMentions !== false), {
|
||||
let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
||||
discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers),
|
||||
...customOptions
|
||||
}, customParser, customHtmlOutput)
|
||||
|
||||
let body = await markdown.toHtmlWithPostParser(content, parsed => transformParsedVia(parsed, false), { // not scanning plaintext body for mentions as we don't parse whether they're in code
|
||||
let body = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
||||
discordCallback: getDiscordParseCallbacks(message, guild, false),
|
||||
discordOnly: true,
|
||||
escapeHTML: false,
|
||||
|
|
@ -656,8 +582,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
// check that condition 1 or 2 is met
|
||||
if (repliedToEventInDifferentRoom || repliedToUnknownEvent) {
|
||||
let referenced = message.referenced_message
|
||||
/* c8 ignore next 4 - backend couldn't be bothered to dereference the message, have to do it ourselves */
|
||||
if (!referenced) {
|
||||
if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves
|
||||
assert(message.message_reference?.message_id)
|
||||
referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id)
|
||||
}
|
||||
|
|
@ -669,7 +594,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
const match = repliedToEventSenderMxid.match(/^@([^:]*)/)
|
||||
assert(match)
|
||||
repliedToDisplayName = referenced.author.username || match[1] || "a Matrix user" // grab the localpart as the display name, whatever
|
||||
repliedToUserHtml = tag`<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
||||
repliedToUserHtml = `<a href="https://matrix.to/#/${repliedToEventSenderMxid}">${repliedToDisplayName}</a>`
|
||||
} else {
|
||||
repliedToDisplayName = referenced.author.global_name || referenced.author.username || "a Discord user"
|
||||
repliedToUserHtml = repliedToDisplayName
|
||||
|
|
@ -694,12 +619,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
+ html
|
||||
body = `${repliedToDisplayName}: ${repliedToBody}`.split("\n").map(line => "> " + line).join("\n") // scenario 1 part B for mentions
|
||||
+ "\n\n" + body
|
||||
} else if (referenced.type === DiscordTypes.MessageType.UserJoin) {
|
||||
// Discord user join messages are bridged as joins, not text events. Generate substitute text for reply.
|
||||
const joinerMxid = select("sim", "mxid", {user_id: referenced.author.id}).pluck().get()
|
||||
const joinerHtml = joinerMxid ? tag`<a href="https://matrix.to/#/${joinerMxid}">${repliedToDisplayName}</a>` : tag`<strong>${repliedToDisplayName}</strong>`
|
||||
html = `<blockquote>${joinerHtml} joined the room</blockquote>` + html
|
||||
body = `> ${repliedToDisplayName} joined the room\n\n` + body
|
||||
} else { // repliedToUnknownEvent
|
||||
const dateDisplay = dUtils.howOldUnbridgedMessage(referenced.timestamp, message.timestamp)
|
||||
html = `<blockquote>In reply to ${dateDisplay} from ${repliedToDisplayName}:`
|
||||
|
|
@ -711,8 +630,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isInteraction && !isThinkingInteraction && message.interaction && events.length === 0) {
|
||||
const formattedInteraction = getFormattedInteraction(message.interaction, false)
|
||||
if (isInteraction && !isThinkingInteraction && events.length === 0) {
|
||||
const formattedInteraction = getFormattedInteraction(interaction, false)
|
||||
body = `${formattedInteraction.body}\n${body}`
|
||||
html = `${formattedInteraction.html}${html}`
|
||||
}
|
||||
|
|
@ -808,37 +727,49 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
events.push(...forwardedEvents)
|
||||
}
|
||||
|
||||
if (isInteraction && isThinkingInteraction && message.interaction) {
|
||||
const formattedInteraction = getFormattedInteraction(message.interaction, true)
|
||||
if (isThinkingInteraction) {
|
||||
const formattedInteraction = getFormattedInteraction(interaction, true)
|
||||
await addTextEvent(formattedInteraction.body, formattedInteraction.html, "m.notice")
|
||||
}
|
||||
|
||||
// Then text content
|
||||
if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) {
|
||||
// Mentions scenario 3: scan the message content for written @mentions of matrix users. Allows for up to one space between @ and mention.
|
||||
let content = message.content
|
||||
if (options.scanTextForMentions !== false) {
|
||||
const matches = [...content.matchAll(/(@ ?)([a-z0-9_.#$][^@\n]+)/gi)]
|
||||
for (let i = matches.length; i--;) {
|
||||
const m = matches[i]
|
||||
const prefix = m[1]
|
||||
const maximumWrittenSection = m[2].toLowerCase()
|
||||
if (m.index > 0 && !content[m.index-1].match(/ |\(|\n/)) continue // must have space before it
|
||||
if (maximumWrittenSection.match(/^everyone\b/) || maximumWrittenSection.match(/^here\b/)) continue // ignore @everyone/@here
|
||||
|
||||
var roomID = roomID ?? select("channel_room", "room_id", {channel_id: message.channel_id}).pluck().get()
|
||||
assert(roomID)
|
||||
var pjr = pjr ?? findMentions.processJoined(Object.entries((await di.api.getJoinedMembers(roomID)).joined).map(([mxid, ev]) => ({mxid, displayname: ev.display_name})))
|
||||
|
||||
const found = findMentions.findMention(pjr, maximumWrittenSection, m.index, prefix, content)
|
||||
if (found) {
|
||||
addMention(found.mxid)
|
||||
content = found.newContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan the content for emojihax and replace them with real emojis
|
||||
let content = message.content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => {
|
||||
content = content.replaceAll(/\[([a-zA-Z0-9_-]{2,32})(?:~[0-9]+)?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/([0-9]+)\.[^ \n)`]+\)/g, (_, name, id) => {
|
||||
return `<:${name}:${id}>`
|
||||
})
|
||||
|
||||
const {body, html} = await transformContent(content, {isTheMessageContent: true})
|
||||
const {body, html} = await transformContent(content)
|
||||
await addTextEvent(body, html, msgtype)
|
||||
}
|
||||
|
||||
// Then scheduled events
|
||||
if (message.content && di?.snow) {
|
||||
for (const match of [...message.content.matchAll(/discord\.gg\/([A-Za-z0-9]+)\?event=([0-9]{18,})/g)]) { // snowflake has minimum 18 because the events feature is at least that old
|
||||
let invite
|
||||
try {
|
||||
invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]})
|
||||
} catch (e) {
|
||||
// Skip expired/invalid invites and events
|
||||
if (e.message === `{"message": "Unknown Invite", "code": 10006}`) {
|
||||
break
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const invite = await di.snow.invite.getInvite(match[1], {guild_scheduled_event_id: match[2]})
|
||||
const event = invite.guild_scheduled_event
|
||||
if (!event) continue // the event ID provided was not valid
|
||||
|
||||
|
|
@ -884,7 +815,15 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
// Try to merge attachment events with the previous event
|
||||
// This means that if the attachments ended up as a text link, and especially if there were many of them, the events will be joined together.
|
||||
mergeTextEvents(attachmentEvents, events, false)
|
||||
let prev = events.at(-1)
|
||||
for (const atch of attachmentEvents) {
|
||||
if (atch.msgtype === "m.text" && prev?.body && prev?.formatted_body && ["m.text", "m.notice"].includes(prev?.msgtype)) {
|
||||
prev.body = prev.body + "\n" + atch.body
|
||||
prev.formatted_body = prev.formatted_body + "<br>" + atch.formatted_body
|
||||
} else {
|
||||
events.push(atch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then components
|
||||
|
|
@ -966,8 +905,11 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
else if (component.type === DiscordTypes.ComponentType.Button) {
|
||||
// May only be a section accessory or in an action row (up to 5)
|
||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||
assert(component.label) // required for Discord to validate link buttons
|
||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||
if (component.label) {
|
||||
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||
} else {
|
||||
stack.msb.add(component.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1022,7 +964,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
|
||||
// Start building up a replica ("rep") of the embed in Discord-markdown format, which we will convert into both plaintext and formatted body at once
|
||||
const rep = new mxUtils.MatrixStringBuilder()
|
||||
let isAdditionalImage = false
|
||||
|
||||
if (isKlipyGIF) {
|
||||
assert(embed.video?.url)
|
||||
|
|
@ -1089,11 +1030,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
let chosenImage = embed.image?.url
|
||||
// the thumbnail seems to be used for "article" type but displayed big at the bottom by discord
|
||||
if (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url
|
||||
|
||||
if (chosenImage) {
|
||||
isAdditionalImage = !rep.body && !!events.length
|
||||
rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
||||
}
|
||||
if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
||||
|
||||
if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`)
|
||||
|
||||
|
|
@ -1102,11 +1039,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
|||
body = body.split("\n").map(l => "| " + l).join("\n")
|
||||
html = `<blockquote>${html}</blockquote>`
|
||||
|
||||
if (isAdditionalImage) {
|
||||
mergeTextEvents([{...rep.get(), body, html, msgtype: "m.notice"}], events, true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Send as m.notice to apply the usual automated/subtle appearance, showing this wasn't actually typed by the person
|
||||
await addTextEvent(body, html, "m.notice")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ test("message2event components: pk question mark output", async t => {
|
|||
+ "<hr>"
|
||||
+ "<blockquote><p><strong>System:</strong> INX (<code>xffgnx</code>)"
|
||||
+ "<br><strong>Member:</strong> Lillith (<code>pphhoh</code>)"
|
||||
+ "<br><strong>Sent by:</strong> infinidoge1337 (<a href=\"https://matrix.to/#/@_ooye_infinidoge1337:cadence.moe\">@unknown-user</a>)"
|
||||
+ "<br><strong>Sent by:</strong> infinidoge1337 (@unknown-user:)"
|
||||
+ "<br><br><strong>Account Roles (7)</strong>"
|
||||
+ "<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>`
|
||||
|
|
|
|||
|
|
@ -125,8 +125,8 @@ test("message2event embeds: blockquote in embed", async t => {
|
|||
t.equal(called, 1, "should call getJoinedMembers once")
|
||||
})
|
||||
|
||||
test("message2event embeds: extreme html is all escaped", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.extreme_html_escaping, data.guild.general)
|
||||
test("message2event embeds: crazy html is all escaped", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.escaping_crazy_html_tags, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
|
|
@ -204,44 +204,6 @@ test("message2event embeds: author url without name", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event embeds: 4 images", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.four_images, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "[🔀 Forwarded message]\n» https://fixupx.com/i/status/2032003668787020046",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "🔀 <em>Forwarded message</em><br><blockquote><a href=\"https://fixupx.com/i/status/2032003668787020046\">https://fixupx.com/i/status/2032003668787020046</a></blockquote>",
|
||||
"m.mentions": {}
|
||||
}, {
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.notice",
|
||||
body: "» | ## ⏺️ AUTOMATON WEST (@AUTOMATON_ENG) https://x.com/AUTOMATON_ENG/status/2032003668787020046"
|
||||
+ "\n» | "
|
||||
+ "\n» | 4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”"
|
||||
+ "\n» | ︀︀"
|
||||
+ "\n» | ︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)"
|
||||
+ "\n» | "
|
||||
+ "\n» | **[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **"
|
||||
+ "\n» | "
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig"
|
||||
+ "\n» | — FixupX"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig"
|
||||
+ "\n» | 📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<blockquote><blockquote><p><strong><a href=\"https://x.com/AUTOMATON_ENG/status/2032003668787020046\">⏺️ AUTOMATON WEST (@AUTOMATON_ENG)</a></strong></p>"
|
||||
+ "<p>4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non-AI made social network”"
|
||||
+ "<br>︀︀<br>︀︀<a href=\"https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/\">automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/</a>"
|
||||
+ "<br><br><strong><a href=\"https://x.com/intent/tweet?in_reply_to=2032003668787020046\">💬</a> 36 <a href=\"https://x.com/intent/retweet?tweet_id=2032003668787020046\">🔁</a> 212 <a href=\"https://x.com/intent/like?tweet_id=2032003668787020046\">❤</a> 3.0K 👁 131.7K </strong></p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig</p>— FixupX</blockquote>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig</p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig</p>"
|
||||
+ "<p>📸 https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig</p></blockquote>",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event embeds: vx image", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ const {MatrixServerError} = require("../../matrix/mreq")
|
|||
const data = require("../../../test/data")
|
||||
const {mockGetEffectivePower} = require("../../matrix/utils.test")
|
||||
const Ty = require("../../types")
|
||||
const {db} = require("../../passthrough")
|
||||
|
||||
/**
|
||||
* @param {string} roomID
|
||||
|
|
@ -734,31 +733,6 @@ test("message2event: reply to a Discord message that wasn't bridged", async t =>
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event: reply to a Discord member join (who didn't join on Matrix)", async t => {
|
||||
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<blockquote><strong>PEASANT!!</strong> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: reply to a Discord member join (who did join on Matrix)", async t => {
|
||||
db.prepare("INSERT INTO sim (user_id, username, sim_name, mxid) VALUES ('1461677775554478161', 'peasant321_76775', 'peasant321_76775', '@_ooye_peasant321_76775:cadence.moe')").run()
|
||||
const events = await messageToEvent(data.message.reply_to_member_join, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
msgtype: "m.text",
|
||||
body: "> PEASANT!! joined the room\n\nwhen the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<blockquote><a href="https://matrix.to/#/@_ooye_peasant321_76775:cadence.moe">PEASANT!!</a> joined the room</blockquote>when the broke friend who we pay to bring food shows up at the medieval lord party`,
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: simple written @mention for matrix user", async t => {
|
||||
const events = await messageToEvent(data.message.simple_written_at_mention_for_matrix, data.guild.general, {}, {
|
||||
api: {
|
||||
|
|
@ -815,7 +789,7 @@ test("message2event: simple written @mention for matrix user", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@ash do you need anything from the store btw as I'm heading there after gym",
|
||||
body: "[@ash](https://matrix.to/#/@she_who_brings_destruction:cadence.moe) do you need anything from the store btw as I'm heading there after gym",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@she_who_brings_destruction:cadence.moe">@ash</a> do you need anything from the store btw as I'm heading there after gym`
|
||||
}])
|
||||
|
|
@ -864,7 +838,7 @@ test("message2event: many written @mentions for matrix users", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@Cadence, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and @huck",
|
||||
body: "[@Cadence](https://matrix.to/#/@cadence:cadence.moe), tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and [@huck](https://matrix.to/#/@huckleton:cadence.moe)",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@cadence:cadence.moe">@Cadence</a>, tell me about @Phil, the creator of the Chin Trick, who has become ever more powerful under the mentorship of @botrac4r and <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a>`
|
||||
}])
|
||||
|
|
@ -916,7 +890,7 @@ test("message2event: written @mentions may match part of the name", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "I wonder if @cadence saw this?",
|
||||
body: "I wonder if [@cadence](https://matrix.to/#/@secret:cadence.moe) saw this?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@secret:cadence.moe">@cadence</a> saw this?`
|
||||
}])
|
||||
|
|
@ -967,7 +941,7 @@ test("message2event: written @mentions may match part of the mxid", async t => {
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "I wonder if @huck saw this?",
|
||||
body: "I wonder if [@huck](https://matrix.to/#/@huckleton:cadence.moe) saw this?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a> saw this?`
|
||||
}])
|
||||
|
|
@ -988,36 +962,6 @@ test("message2event: written @mentions do not match in URLs", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event: written @mentions do not match in inline code", async t => {
|
||||
const events = await messageToEvent({
|
||||
...data.message.advanced_written_at_mention_for_matrix,
|
||||
content: "`public @Nullable EntityType<?>`"
|
||||
}, data.guild.general, {}, {})
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
body: "`public @Nullable EntityType<?>`",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<code>public @Nullable EntityType<?></code>`
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: written @mentions do not match in code block", async t => {
|
||||
const events = await messageToEvent({
|
||||
...data.message.advanced_written_at_mention_for_matrix,
|
||||
content: "```java\npublic @Nullable EntityType<?>\n```"
|
||||
}, data.guild.general, {}, {})
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
body: "```java\npublic @Nullable EntityType<?>\n```",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<pre><code class="language-java">public @Nullable EntityType<?></code></pre>`
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: entire message may match elaborate display name", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent({
|
||||
|
|
@ -1063,7 +1007,7 @@ test("message2event: entire message may match elaborate display name", async t =
|
|||
]
|
||||
},
|
||||
msgtype: "m.text",
|
||||
body: "@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆",
|
||||
body: "[@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆](https://matrix.to/#/@wa:cadence.moe)",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<a href="https://matrix.to/#/@wa:cadence.moe">@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆</a>`
|
||||
}])
|
||||
|
|
@ -1140,7 +1084,7 @@ test("message2event: multiple attachments are combined into the same event where
|
|||
formatted_body: "hey"
|
||||
+ `<br>📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
+ `<br><blockquote>📸 Uploaded SPOILER file: <a href="https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg">https://bridge.example.org/download/discordcdn/123/456/SPOILER_secret.jpg</a> (38 KB)</blockquote>`
|
||||
+ `📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
+ `<br>📄 Uploaded file: <a href="https://bridge.example.org/download/discordcdn/123/456/789.mega">hey.jpg</a> (100 MB)`
|
||||
}, {
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
|
|
@ -1168,19 +1112,6 @@ test("message2event: type 4 channel name change", async t => {
|
|||
}])
|
||||
})
|
||||
|
||||
test("message2event: type 12 channel follow add", async t => {
|
||||
const events = await messageToEvent(data.special_message.channel_follow_add, data.guild.general)
|
||||
t.deepEqual(events, [{
|
||||
$type: "m.room.message",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.emote",
|
||||
body: "set this room to receive announcements from PluralKit #downtime",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "set this room to receive announcements from <strong>PluralKit #downtime</strong>",
|
||||
"m.mentions": {}
|
||||
}])
|
||||
})
|
||||
|
||||
test("message2event: thread start message reference", async t => {
|
||||
const events = await messageToEvent(data.special_message.thread_start_context, data.guild.general, {}, {
|
||||
api: {
|
||||
|
|
@ -1607,28 +1538,6 @@ test("message2event: vc invite event renders embed with room link", async t => {
|
|||
])
|
||||
})
|
||||
|
||||
test("message2event: expired/invalid invites are sent as-is", async t => {
|
||||
const events = await messageToEvent({content: "https://discord.gg/placeholder?event=1381190945646710824"}, {}, {}, {
|
||||
snow: {
|
||||
invite: {
|
||||
async getInvite() {
|
||||
throw new Error(`{"message": "Unknown Invite", "code": 10006}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.deepEqual(events, [
|
||||
{
|
||||
$type: "m.room.message",
|
||||
body: "https://discord.gg/placeholder?event=1381190945646710824",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<a href=\"https://discord.gg/placeholder?event=1381190945646710824\">https://discord.gg/placeholder?event=1381190945646710824</a>",
|
||||
"m.mentions": {},
|
||||
msgtype: "m.text",
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
test("message2event: channel links are converted even inside lists (parser post-processer descends into list items)", async t => {
|
||||
let called = 0
|
||||
const events = await messageToEvent({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function pinsToList(pins, kstate) {
|
|||
/** @type {string[]} */
|
||||
const result = []
|
||||
for (const pin of pins.items) {
|
||||
const eventID = select("event_message", "event_id", {message_id: pin.message.id}, "ORDER BY part ASC").pluck().get()
|
||||
const eventID = select("event_message", "event_id", {message_id: pin.message.id, part: 0}).pluck().get()
|
||||
if (eventID && !alreadyPinned.includes(eventID)) result.push(eventID)
|
||||
}
|
||||
result.reverse()
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
// @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
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// @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'
|
||||
}]
|
||||
})
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const {test} = require("supertape")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert")
|
||||
const data = require("../../../test/data")
|
||||
const {userToSimName, webhookAuthorToSimName} = require("./user-to-mxid")
|
||||
|
|
|
|||
|
|
@ -52,11 +52,7 @@ class DiscordClient {
|
|||
/** @type {Map<string, Array<string>>} */
|
||||
this.guildChannelMap = new Map()
|
||||
if (listen !== "no") {
|
||||
this.cloud.on("event", message => {
|
||||
process.nextTick(() => {
|
||||
discordPackets.onPacket(this, message, listen)
|
||||
})
|
||||
})
|
||||
this.cloud.on("event", message => discordPackets.onPacket(this, message, listen))
|
||||
}
|
||||
|
||||
const addEventLogger = (eventName, logName) => {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const utils = {
|
|||
client.user = message.d.user
|
||||
client.application = message.d.application
|
||||
console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
|
||||
interactions.registerInteractions()
|
||||
|
||||
} else if (message.t === "GUILD_CREATE") {
|
||||
message.d.members = message.d.members.filter(m => m.user.id === client.user.id) // only keep the bot's own member - it's needed to determine private channels on web
|
||||
|
|
@ -48,10 +47,10 @@ const utils = {
|
|||
|
||||
if (listen === "full") {
|
||||
try {
|
||||
interactions.registerInteractions()
|
||||
await eventDispatcher.checkMissedExpressions(message.d)
|
||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||
await eventDispatcher.checkMissedPins(client, message.d)
|
||||
await eventDispatcher.checkMissedLeaves(client, message.d)
|
||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||
} catch (e) {
|
||||
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
||||
console.error(e)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ const speedbump = sync.require("./actions/speedbump")
|
|||
const retrigger = sync.require("./actions/retrigger")
|
||||
/** @type {import("./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")} */
|
||||
const vote = sync.require("./actions/poll-vote")
|
||||
/** @type {import("../m2d/event-dispatcher")} */
|
||||
|
|
@ -125,7 +123,6 @@ module.exports = {
|
|||
// Send in order
|
||||
for (let i = Math.min(messages.length, latestBridgedMessageIndex)-1; i >= 0; 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))
|
||||
await module.exports.MESSAGE_CREATE(client, {
|
||||
|
|
@ -175,31 +172,6 @@ module.exports = {
|
|||
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.
|
||||
* See notes.md, "Ignore MESSAGE_UPDATE and bridge THREAD_CREATE as the announcement"
|
||||
|
|
@ -239,14 +211,6 @@ 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 {DiscordTypes.GatewayChannelUpdateDispatchData} channelOrThread
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE "role_default" (
|
||||
"guild_id" TEXT NOT NULL,
|
||||
"role_id" TEXT NOT NULL,
|
||||
PRIMARY KEY ("guild_id", "role_id")
|
||||
) WITHOUT ROWID;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
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;
|
||||
11
src/db/orm-defs.d.ts
vendored
11
src/db/orm-defs.d.ts
vendored
|
|
@ -1,10 +1,4 @@
|
|||
export type Models = {
|
||||
app_user_install: {
|
||||
guild_id: string
|
||||
app_bot_id: string
|
||||
user_id: string
|
||||
}
|
||||
|
||||
auto_emoji: {
|
||||
name: string
|
||||
emoji_id: string
|
||||
|
|
@ -110,11 +104,6 @@ export type Models = {
|
|||
historical_room_index: number
|
||||
}
|
||||
|
||||
role_default: {
|
||||
guild_id: string
|
||||
role_id: string
|
||||
}
|
||||
|
||||
room_upgrade_pending: {
|
||||
new_room_id: string
|
||||
old_room_id: string
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ async function _interact({guild_id, data}, {api}) {
|
|||
// from Matrix
|
||||
const event = await api.getEvent(message.room_id, message.event_id)
|
||||
const via = await utils.getViaServersQuery(message.room_id, api)
|
||||
|
||||
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
||||
assert(channelsInGuild)
|
||||
const inChannels = channelsInGuild
|
||||
|
|
@ -62,35 +61,8 @@ async function _interact({guild_id, data}, {api}) {
|
|||
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
||||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
||||
let inChannelsText = inChannels.map(c => `<#${c.id}>`).join(" • ")
|
||||
if (inChannelsText.length > 1024) {
|
||||
inChannelsText = `In ${inChannels.length} channels`
|
||||
}
|
||||
|
||||
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
||||
let name = matrixMember?.displayname || event.sender
|
||||
let avatar = utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||
|
||||
// Check for per-message profile
|
||||
const perMessageProfile = event.content?.["com.beeper.per_message_profile"]
|
||||
let profileNote = ""
|
||||
if (perMessageProfile) {
|
||||
if (perMessageProfile.displayname) {
|
||||
name = perMessageProfile.displayname
|
||||
}
|
||||
if ("avatar_url" in perMessageProfile) {
|
||||
if (perMessageProfile.avatar_url) {
|
||||
// use provided avatar_url
|
||||
avatar = utils.getPublicUrlForMxc(perMessageProfile.avatar_url)
|
||||
} else if (perMessageProfile.avatar_url === "") {
|
||||
// empty string avatar_url clears the avatar
|
||||
avatar = undefined
|
||||
}
|
||||
// else, omitted/null falls back to member avatar
|
||||
}
|
||||
profileNote = "Sent with a per-message profile.\n"
|
||||
}
|
||||
|
||||
const name = matrixMember?.displayname || event.sender
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
|
|
@ -98,13 +70,13 @@ async function _interact({guild_id, data}, {api}) {
|
|||
author: {
|
||||
name,
|
||||
url: `https://matrix.to/#/${event.sender}`,
|
||||
icon_url: avatar
|
||||
icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||
},
|
||||
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n${profileNote}**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
||||
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
||||
color: 0x0dbd8b,
|
||||
fields: [{
|
||||
name: "In Channels",
|
||||
value: inChannelsText
|
||||
value: inChannels.map(c => `<#${c.id}>`).join(" • ")
|
||||
}, {
|
||||
name: "\u200b",
|
||||
value: idInfo
|
||||
|
|
|
|||
|
|
@ -85,118 +85,3 @@ test("matrix info: shows info for matrix source message", async t => {
|
|||
)
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("matrix info: shows username for per-message profile", async t => {
|
||||
let called = 0
|
||||
const msg = await _interact({
|
||||
data: {
|
||||
target_id: "1128118177155526666",
|
||||
resolved: {
|
||||
messages: {
|
||||
"1141501302736695316": data.message.simple_reply_to_matrix_user
|
||||
}
|
||||
}
|
||||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
return {
|
||||
event_id: eventID,
|
||||
room_id: roomID,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "master chief: i like the halo",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong>master chief: </strong>i like the halo",
|
||||
"com.beeper.per_message_profile": {
|
||||
has_fallback: true,
|
||||
displayname: "master chief",
|
||||
avatar_url: ""
|
||||
}
|
||||
},
|
||||
sender: "@cadence:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getJoinedMembers(roomID) {
|
||||
return {
|
||||
joined: {}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
return {
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(msg.data.embeds[0].author.name, "master chief")
|
||||
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
||||
test("matrix info: shows avatar for per-message profile", async t => {
|
||||
let called = 0
|
||||
const msg = await _interact({
|
||||
data: {
|
||||
target_id: "1128118177155526666",
|
||||
resolved: {
|
||||
messages: {
|
||||
"1141501302736695316": data.message.simple_reply_to_matrix_user
|
||||
}
|
||||
}
|
||||
},
|
||||
guild_id: "112760669178241024"
|
||||
}, {
|
||||
api: {
|
||||
async getEvent(roomID, eventID) {
|
||||
called++
|
||||
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe")
|
||||
t.equal(eventID, "$Ij3qo7NxMA4VPexlAiIx2CB9JbsiGhJeyt-2OvkAUe4")
|
||||
return {
|
||||
event_id: eventID,
|
||||
room_id: roomID,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "?",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "?",
|
||||
"com.beeper.per_message_profile": {
|
||||
avatar_url: "mxc://cadence.moe/HXfFuougamkURPPMflTJRxGc"
|
||||
}
|
||||
},
|
||||
sender: "@mystery:cadence.moe"
|
||||
}
|
||||
},
|
||||
async getJoinedMembers(roomID) {
|
||||
return {
|
||||
joined: {}
|
||||
}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
return {
|
||||
content: {
|
||||
room_version: "11"
|
||||
}
|
||||
}
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
t.equal(msg.data.embeds[0].author.name, "@mystery:cadence.moe")
|
||||
t.equal(msg.data.embeds[0].author.icon_url, "https://bridge.example.org/download/matrix/cadence.moe/HXfFuougamkURPPMflTJRxGc")
|
||||
t.match(msg.data.embeds[0].description, "Sent with a per-message profile")
|
||||
t.equal(called, 1)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const assert = require("assert").strict
|
|||
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
|
||||
const {db, select} = require("../passthrough")
|
||||
const {db} = require("../passthrough")
|
||||
|
||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||
let hasher = null
|
||||
|
|
@ -58,15 +58,6 @@ function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrite
|
|||
return allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{id: string, roles: DiscordTypes.APIGuild["roles"]}} guild
|
||||
* @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channel]
|
||||
*/
|
||||
function getDefaultPermissions(guild, channel) {
|
||||
const defaultRoles = select("role_default", "role_id", {guild_id: guild.id}).pluck().all()
|
||||
return getPermissions(guild.id, defaultRoles, guild.roles, undefined, channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`.
|
||||
* It is designed like this to avoid developer error with bit manipulations.
|
||||
|
|
@ -114,7 +105,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
|
|||
* @param {DiscordTypes.APIMessage} message
|
||||
*/
|
||||
function isWebhookMessage(message) {
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand && message.type !== DiscordTypes.MessageType.ContextMenuCommand
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -182,396 +173,7 @@ function filterTo(xs, fn) {
|
|||
return filtered
|
||||
}
|
||||
|
||||
const supportedPlaintextPreviewExtensions = new Set([
|
||||
"4d",
|
||||
"abnf",
|
||||
"accesslog",
|
||||
"actionscript",
|
||||
"ada",
|
||||
"adoc",
|
||||
"alan",
|
||||
"angelscript",
|
||||
"ansi",
|
||||
"apache",
|
||||
"apacheconf",
|
||||
"applescript",
|
||||
"arcade",
|
||||
"arduino",
|
||||
"arm",
|
||||
"armasm",
|
||||
"as",
|
||||
"asc",
|
||||
"asciidoc",
|
||||
"aspectj",
|
||||
"ass",
|
||||
"atom",
|
||||
"autohotkey",
|
||||
"autoit",
|
||||
"avrasm",
|
||||
"awk",
|
||||
"axapta",
|
||||
"bash",
|
||||
"basic",
|
||||
"bat",
|
||||
"bbcode",
|
||||
"bf",
|
||||
"bind",
|
||||
"blade",
|
||||
"bnf",
|
||||
"brainfuck",
|
||||
"c",
|
||||
"c++",
|
||||
"cal",
|
||||
"capnp",
|
||||
"capnproto",
|
||||
"cc",
|
||||
"chaos",
|
||||
"chapel",
|
||||
"chpl",
|
||||
"cisco",
|
||||
"clj",
|
||||
"clojure",
|
||||
"cls",
|
||||
"cmake.in",
|
||||
"cmake",
|
||||
"cmd",
|
||||
"coffee",
|
||||
"coffeescript",
|
||||
"console",
|
||||
"coq",
|
||||
"cos",
|
||||
"cpc",
|
||||
"cpp",
|
||||
"cr",
|
||||
"craftcms",
|
||||
"crm",
|
||||
"crmsh",
|
||||
"crystal",
|
||||
"cs",
|
||||
"csharp",
|
||||
"cshtml",
|
||||
"cson",
|
||||
"csp",
|
||||
"css",
|
||||
"csv",
|
||||
"cxx",
|
||||
"cypher",
|
||||
"d",
|
||||
"dart",
|
||||
"delphi",
|
||||
"dfm",
|
||||
"diff",
|
||||
"django",
|
||||
"dns",
|
||||
"docker",
|
||||
"dockerfile",
|
||||
"dos",
|
||||
"dpr",
|
||||
"dsconfig",
|
||||
"dst",
|
||||
"dts",
|
||||
"dust",
|
||||
"dylan",
|
||||
"ebnf",
|
||||
"elixir",
|
||||
"elm",
|
||||
"erl",
|
||||
"erlang",
|
||||
"ex",
|
||||
"extempore",
|
||||
"f90",
|
||||
"f95",
|
||||
"fix",
|
||||
"fortran",
|
||||
"freepascal",
|
||||
"fs",
|
||||
"fsharp",
|
||||
"gams",
|
||||
"gauss",
|
||||
"gawk",
|
||||
"gcode",
|
||||
"gdscript",
|
||||
"gemspec",
|
||||
"gf",
|
||||
"gherkin",
|
||||
"glsl",
|
||||
"gms",
|
||||
"gn",
|
||||
"gni",
|
||||
"go",
|
||||
"godot",
|
||||
"golang",
|
||||
"golo",
|
||||
"gololang",
|
||||
"gradle",
|
||||
"graph",
|
||||
"groovy",
|
||||
"gss",
|
||||
"gyp",
|
||||
"h",
|
||||
"h++",
|
||||
"haml",
|
||||
"handlebars",
|
||||
"haskell",
|
||||
"haxe",
|
||||
"hbs",
|
||||
"hcl",
|
||||
"hh",
|
||||
"hpp",
|
||||
"hs",
|
||||
"html.handlebars",
|
||||
"html.hbs",
|
||||
"html",
|
||||
"http",
|
||||
"https",
|
||||
"hx",
|
||||
"hxx",
|
||||
"hy",
|
||||
"hylang",
|
||||
"i",
|
||||
"i7",
|
||||
"iced",
|
||||
"iecst",
|
||||
"inform7",
|
||||
"ini",
|
||||
"ino",
|
||||
"instances",
|
||||
"iol",
|
||||
"irb",
|
||||
"irpf90",
|
||||
"java",
|
||||
"javascript",
|
||||
"jinja",
|
||||
"jolie",
|
||||
"js",
|
||||
"json",
|
||||
"jsp",
|
||||
"jsx",
|
||||
"julia-repl",
|
||||
"julia",
|
||||
"k",
|
||||
"kaos",
|
||||
"kdb",
|
||||
"kotlin",
|
||||
"kt",
|
||||
"lasso",
|
||||
"lassoscript",
|
||||
"lazarus",
|
||||
"ldif",
|
||||
"leaf",
|
||||
"lean",
|
||||
"less",
|
||||
"lfm",
|
||||
"lisp",
|
||||
"livecodeserver",
|
||||
"livescript",
|
||||
"ln",
|
||||
"lock",
|
||||
"log",
|
||||
"lpr",
|
||||
"ls",
|
||||
"ls",
|
||||
"lua",
|
||||
"mak",
|
||||
"make",
|
||||
"makefile",
|
||||
"markdown",
|
||||
"mathematica",
|
||||
"matlab",
|
||||
"mawk",
|
||||
"maxima",
|
||||
"md",
|
||||
"mel",
|
||||
"mercury",
|
||||
"mirc",
|
||||
"mizar",
|
||||
"mk",
|
||||
"mkd",
|
||||
"mkdown",
|
||||
"ml",
|
||||
"ml",
|
||||
"mm",
|
||||
"mma",
|
||||
"mojolicious",
|
||||
"monkey",
|
||||
"moon",
|
||||
"moonscript",
|
||||
"mrc",
|
||||
"n1ql",
|
||||
"nawk",
|
||||
"nc",
|
||||
"never",
|
||||
"nginx",
|
||||
"nginxconf",
|
||||
"nim",
|
||||
"nimrod",
|
||||
"nix",
|
||||
"nsis",
|
||||
"obj-c",
|
||||
"obj-c++",
|
||||
"objc",
|
||||
"objective-c++",
|
||||
"objectivec",
|
||||
"ocaml",
|
||||
"ocl",
|
||||
"ol",
|
||||
"openscad",
|
||||
"osascript",
|
||||
"oxygene",
|
||||
"p21",
|
||||
"parser3",
|
||||
"pas",
|
||||
"pascal",
|
||||
"patch",
|
||||
"pcmk",
|
||||
"perl",
|
||||
"pf.conf",
|
||||
"pf",
|
||||
"pgsql",
|
||||
"php",
|
||||
"php3",
|
||||
"php4",
|
||||
"php5",
|
||||
"php6",
|
||||
"php7",
|
||||
"pl",
|
||||
"plaintext",
|
||||
"plist",
|
||||
"pm",
|
||||
"podspec",
|
||||
"pony",
|
||||
"postgres",
|
||||
"postgresql",
|
||||
"powershell",
|
||||
"pp",
|
||||
"processing",
|
||||
"profile",
|
||||
"prolog",
|
||||
"properties",
|
||||
"proto",
|
||||
"protobuf",
|
||||
"ps",
|
||||
"ps1",
|
||||
"puppet",
|
||||
"py",
|
||||
"pycon",
|
||||
"python-repl",
|
||||
"python",
|
||||
"qml",
|
||||
"r",
|
||||
"razor-cshtml",
|
||||
"razor",
|
||||
"rb",
|
||||
"re",
|
||||
"reasonml",
|
||||
"rebol",
|
||||
"red-system",
|
||||
"red",
|
||||
"redbol",
|
||||
"rf",
|
||||
"rib",
|
||||
"robot",
|
||||
"rpm-spec",
|
||||
"rpm-specfile",
|
||||
"rpm",
|
||||
"rs",
|
||||
"rsl",
|
||||
"rss",
|
||||
"ruby",
|
||||
"ruleslanguage",
|
||||
"rust",
|
||||
"sas",
|
||||
"SAS",
|
||||
"sc",
|
||||
"scad",
|
||||
"scala",
|
||||
"scheme",
|
||||
"sci",
|
||||
"scilab",
|
||||
"scl",
|
||||
"scss",
|
||||
"sh",
|
||||
"shell",
|
||||
"shexc",
|
||||
"smali",
|
||||
"smalltalk",
|
||||
"sml",
|
||||
"sol",
|
||||
"solidity",
|
||||
"spec",
|
||||
"specfile",
|
||||
"sql",
|
||||
"srt",
|
||||
"ssa",
|
||||
"st",
|
||||
"stan",
|
||||
"stanfuncs",
|
||||
"stata",
|
||||
"step",
|
||||
"stp",
|
||||
"structured-text",
|
||||
"styl",
|
||||
"stylus",
|
||||
"subunit",
|
||||
"supercollider",
|
||||
"svelte",
|
||||
"svg",
|
||||
"swift",
|
||||
"tao",
|
||||
"tap",
|
||||
"tcl",
|
||||
"terraform",
|
||||
"tex",
|
||||
"text",
|
||||
"tf",
|
||||
"thor",
|
||||
"thrift",
|
||||
"tk",
|
||||
"toml",
|
||||
"tp",
|
||||
"ts",
|
||||
"tsql",
|
||||
"tsx",
|
||||
"ttml",
|
||||
"twig",
|
||||
"txt",
|
||||
"typescript",
|
||||
"unicorn-rails-log",
|
||||
"v",
|
||||
"vala",
|
||||
"vb",
|
||||
"vba",
|
||||
"vbnet",
|
||||
"vbs",
|
||||
"vbscript",
|
||||
"verilog",
|
||||
"vhdl",
|
||||
"vim",
|
||||
"vtt",
|
||||
"wl",
|
||||
"x++",
|
||||
"x86asm",
|
||||
"xhtml",
|
||||
"xjb",
|
||||
"xl",
|
||||
"xml",
|
||||
"xpath",
|
||||
"xq",
|
||||
"xquery",
|
||||
"xsd",
|
||||
"xsl",
|
||||
"xtlang",
|
||||
"xtm",
|
||||
"yaml",
|
||||
"yml",
|
||||
"zep",
|
||||
"zephir",
|
||||
"zone",
|
||||
"zsh"
|
||||
])
|
||||
|
||||
module.exports.getPermissions = getPermissions
|
||||
module.exports.getDefaultPermissions = getDefaultPermissions
|
||||
module.exports.hasPermission = hasPermission
|
||||
module.exports.hasSomePermissions = hasSomePermissions
|
||||
module.exports.hasAllPermissions = hasAllPermissions
|
||||
|
|
@ -582,4 +184,3 @@ module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
|
|||
module.exports.getPublicUrlForCdn = getPublicUrlForCdn
|
||||
module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage
|
||||
module.exports.filterTo = filterTo
|
||||
module.exports.supportedPlaintextPreviewExtensions = supportedPlaintextPreviewExtensions
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const sharp = require("sharp")
|
|||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../../matrix/mreq")} */
|
||||
const mreq = sync.require("../../matrix/mreq")
|
||||
const {streamType} = require("@cloudrac3r/stream-type")
|
||||
const streamMimeType = require("stream-mime-type")
|
||||
|
||||
const WIDTH = 160
|
||||
const HEIGHT = 160
|
||||
|
|
@ -26,13 +26,13 @@ async function getAndResizeSticker(mxc) {
|
|||
}
|
||||
|
||||
const streamIn = Readable.fromWeb(res.body)
|
||||
const {streamThrough, type} = await streamType(streamIn)
|
||||
const animated = ["image/gif", "image/webp"].includes(type)
|
||||
const { stream, mime } = await streamMimeType.getMimeType(streamIn)
|
||||
const animated = ["image/gif", "image/webp"].includes(mime)
|
||||
|
||||
const transformer = sharp({animated: animated})
|
||||
.resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}})
|
||||
.webp()
|
||||
streamThrough.pipe(transformer)
|
||||
stream.pipe(transformer)
|
||||
return Readable.toWeb(transformer)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function updatePins(pins, prev) {
|
|||
const diff = diffPins.diffPins(pins, prev)
|
||||
for (const [event_id, added] of diff) {
|
||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||
.select("reference_channel_id", "message_id").where({event_id}).and("ORDER BY part ASC").get()
|
||||
.select("reference_channel_id", "message_id").get()
|
||||
if (!row) continue
|
||||
if (added) {
|
||||
discord.snow.channel.addChannelPinnedMessage(row.reference_channel_id, row.message_id, "Message pinned on Matrix")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const {pipeline} = require("stream").promises
|
|||
const sharp = require("sharp")
|
||||
const {GIFrame} = require("@cloudrac3r/giframe")
|
||||
const {PNG} = require("@cloudrac3r/pngjs")
|
||||
const {streamType} = require("@cloudrac3r/stream-type")
|
||||
const streamMimeType = require("stream-mime-type")
|
||||
|
||||
const SIZE = 48
|
||||
const RESULT_WIDTH = 400
|
||||
|
|
@ -54,11 +54,11 @@ async function compositeMatrixEmojis(mxcs, mxcDownloader) {
|
|||
* @returns {Promise<Buffer | undefined>} Uncompressed PNG image
|
||||
*/
|
||||
async function convertImageStream(streamIn, stopStream) {
|
||||
const {streamThrough, type} = await streamType(streamIn)
|
||||
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(type), `Mime type ${type} is impossible for emojis`)
|
||||
const {stream, mime} = await streamMimeType.getMimeType(streamIn)
|
||||
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(mime), `Mime type ${mime} is impossible for emojis`)
|
||||
|
||||
try {
|
||||
if (type === "image/png" || type === "image/jpeg" || type === "image/webp") {
|
||||
if (mime === "image/png" || mime === "image/jpeg" || mime === "image/webp") {
|
||||
/** @type {{info: sharp.OutputInfo, buffer: Buffer}} */
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const transformer = sharp()
|
||||
|
|
@ -70,15 +70,15 @@ async function convertImageStream(streamIn, stopStream) {
|
|||
resolve({info, buffer})
|
||||
})
|
||||
pipeline(
|
||||
streamThrough,
|
||||
stream,
|
||||
transformer
|
||||
)
|
||||
})
|
||||
return result.buffer
|
||||
|
||||
} else if (type === "image/gif") {
|
||||
} else if (mime === "image/gif") {
|
||||
const giframe = new GIFrame(0)
|
||||
streamThrough.on("data", chunk => {
|
||||
stream.on("data", chunk => {
|
||||
giframe.feed(chunk)
|
||||
})
|
||||
const frame = await giframe.getFrame()
|
||||
|
|
@ -91,10 +91,10 @@ async function convertImageStream(streamIn, stopStream) {
|
|||
.toBuffer({resolveWithObject: true})
|
||||
return buffer.data
|
||||
|
||||
} else if (type === "image/apng") {
|
||||
} else if (mime === "image/apng") {
|
||||
const png = new PNG({maxFrames: 1})
|
||||
// @ts-ignore
|
||||
streamThrough.pipe(png)
|
||||
stream.pipe(png)
|
||||
/** @type {Buffer} */ // @ts-ignore
|
||||
const frame = await new Promise(resolve => png.on("parsed", resolve))
|
||||
stopStream()
|
||||
|
|
|
|||
|
|
@ -471,8 +471,7 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
|||
// @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]),
|
||||
ensureJoined: [],
|
||||
allowedMentionsParse: ["everyone"],
|
||||
allowedMentionsUsers: []
|
||||
allowedMentionsParse: ["everyone"]
|
||||
}
|
||||
}
|
||||
} 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
|
||||
|
|
@ -483,8 +482,7 @@ async function checkWrittenMentions(content, senderMxid, roomID, guild, di) {
|
|||
// @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]),
|
||||
ensureJoined: [results[0].user],
|
||||
allowedMentionsParse: [],
|
||||
allowedMentionsUsers: [results[0].user.id]
|
||||
allowedMentionsParse: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -546,34 +544,16 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
let displayName = event.sender
|
||||
let avatarURL = undefined
|
||||
const allowedMentionsParse = ["users", "roles"]
|
||||
const allowedMentionsUsers = []
|
||||
/** @type {string[]} */
|
||||
let messageIDsToEdit = []
|
||||
let replyLine = ""
|
||||
|
||||
// Extract a basic display name from the sender
|
||||
const match = event.sender.match(/^@(.*?):/)
|
||||
if (match) displayName = match[1]
|
||||
|
||||
// Try to extract an accurate display name and avatar URL from the member event
|
||||
const member = await getMemberFromCacheOrHomeserver(event.room_id, event.sender, di?.api)
|
||||
if (member.displayname) displayName = member.displayname
|
||||
if (member.avatar_url) avatarURL = mxUtils.getPublicUrlForMxc(member.avatar_url)
|
||||
|
||||
// MSC4144: Override display name and avatar from per-message profile if present
|
||||
const perMessageProfile = event.content["com.beeper.per_message_profile"]
|
||||
if (perMessageProfile?.displayname) displayName = perMessageProfile.displayname
|
||||
if (perMessageProfile && "avatar_url" in perMessageProfile) {
|
||||
if (perMessageProfile.avatar_url) {
|
||||
// use provided avatar_url
|
||||
avatarURL = mxUtils.getPublicUrlForMxc(perMessageProfile.avatar_url)
|
||||
} else if (perMessageProfile.avatar_url === "") {
|
||||
// empty string avatar_url clears the avatar
|
||||
avatarURL = undefined
|
||||
}
|
||||
// else, omitted/null falls back to member avatar
|
||||
}
|
||||
|
||||
// If the display name is too long to be put into the webhook (80 characters is the maximum),
|
||||
// put the excess characters into displayNameRunoff, later to be put at the top of the message
|
||||
let [displayNameShortened, displayNameRunoff] = splitDisplayName(displayName)
|
||||
|
|
@ -783,7 +763,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
// 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(/^\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(/<img([^>]*)>/g, (_, att) => { // Convert Matrix emoji images into Discord emoji markdown
|
||||
const mxcUrlMatch = att.match(/\bsrc="(mxc:\/\/[^"]+)"/)
|
||||
|
|
@ -876,9 +856,8 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
const doc = domino.createDocument(
|
||||
// DOM parsers arrange elements in the <head> and <body>. Wrapping in a custom element ensures elements are reliably arranged in a single element.
|
||||
'<x-turndown id="turndown-root">' + input + '</x-turndown>'
|
||||
)
|
||||
const root = doc.getElementById("turndown-root")
|
||||
assert(root)
|
||||
);
|
||||
const root = doc.getElementById("turndown-root");
|
||||
async function forEachNode(event, node) {
|
||||
for (; node; node = node.nextSibling) {
|
||||
// Check written mentions
|
||||
|
|
@ -894,8 +873,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
let preNode
|
||||
if (node.nodeType === 3 && node.nodeValue.includes("```") && (preNode = nodeIsChildOf(node, ["PRE"]))) {
|
||||
if (preNode.firstChild?.nodeName === "CODE") {
|
||||
let ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1]
|
||||
if (!dUtils.supportedPlaintextPreviewExtensions.has(ext)) ext = "txt"
|
||||
const ext = preNode.firstChild.className.match(/language-(\S+)/)?.[1] || "txt"
|
||||
const filename = `inline_code.${ext}`
|
||||
// Build the replacement <code> node
|
||||
const replacementCode = doc.createElement("code")
|
||||
|
|
@ -920,7 +898,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
// Suppress if regular users don't have permission
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel?.permission_overwrites)
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
|
|
@ -932,7 +910,6 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
}
|
||||
await forEachNode(event, root)
|
||||
if (perMessageProfile?.has_fallback) root.querySelectorAll("[data-mx-profile-fallback]").forEach(x => x.remove())
|
||||
|
||||
// SPRITE SHEET EMOJIS FEATURE: Emojis at the end of the message that we don't know about will be reuploaded as a sprite sheet.
|
||||
// First we need to determine which emojis are at the end.
|
||||
|
|
@ -964,10 +941,6 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
} else {
|
||||
// Looks like we're using the plaintext body!
|
||||
content = event.content.body
|
||||
if (perMessageProfile?.has_fallback && perMessageProfile.displayname && content.startsWith(perMessageProfile.displayname + ": ")) {
|
||||
// Strip the display name prefix fallback added for clients that don't support per-message profiles
|
||||
content = content.slice(perMessageProfile.displayname.length + 2)
|
||||
}
|
||||
|
||||
if (event.content.msgtype === "m.emote") {
|
||||
content = `* ${displayName} ${content}`
|
||||
|
|
@ -988,7 +961,7 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
|
||||
// Suppress if regular users don't have permission
|
||||
if (!shouldSuppress && guild?.roles) {
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel.permission_overwrites)
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles, undefined, channel.permission_overwrites)
|
||||
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||
shouldSuppress = !canEmbedLinks
|
||||
}
|
||||
|
|
@ -1013,34 +986,16 @@ async function eventToMessage(event, guild, channel, di) {
|
|||
}
|
||||
}
|
||||
|
||||
// Complete content
|
||||
content = displayNameRunoff + replyLine + content
|
||||
|
||||
// Split into 2000 character chunks
|
||||
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}[]})[]} */
|
||||
const messages = chunks.map(content => ({
|
||||
content,
|
||||
allowed_mentions,
|
||||
allowed_mentions: {
|
||||
parse: allowedMentionsParse
|
||||
},
|
||||
username: displayNameShortened,
|
||||
avatar_url: avatarURL
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -266,8 +266,7 @@ 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?",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -548,8 +547,7 @@ 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",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1155,38 +1153,6 @@ test("event2message: code blocks are uploaded as attachments instead if they con
|
|||
)
|
||||
})
|
||||
|
||||
test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (use txt extension if discord does not recognise the language)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "wrong body",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: 'So if you run code like this<pre><code class="language-if">System.out.println("```");</code></pre>it should print a markdown formatted code block'
|
||||
},
|
||||
event_id: "$pGkWQuGVmrPNByrFELxhzI6MCBgJecr5I2J3z88Gc2s",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "So if you run code like this `[inline_code.txt]` it should print a markdown formatted code block",
|
||||
attachments: [{id: "0", filename: "inline_code.txt"}],
|
||||
pendingFiles: [{name: "inline_code.txt", buffer: Buffer.from('System.out.println("```");')}],
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: code blocks are uploaded as attachments instead if they contain incompatible backticks (default to txt file extension)", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
@ -1330,8 +1296,7 @@ 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`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1372,8 +1337,7 @@ 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`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["roles"],
|
||||
users: []
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -1499,118 +1463,6 @@ 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 => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
@ -1975,9 +1827,9 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
|||
sender: "@rnl:cadence.moe",
|
||||
content: {
|
||||
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 "can't believe 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 "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
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 "can't believe 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 "crazy that this came out before the scandal" but I thought Mr Beast sucking was a common sentiment`,
|
||||
"m.relates_to": {
|
||||
"m.in_reply_to": {
|
||||
event_id: "$qmyjr-ISJtnOM5WTWLI0fT7uSlqRLgpyin2d2NCglCU"
|
||||
|
|
@ -2007,7 +1859,7 @@ test("event2message: should suppress embeds for links in reply preview", async t
|
|||
username: "RNL",
|
||||
content: "-# > <:L1:1144820033948762203><:L2:1144820084079087647>https://discord.com/channels/112760669178241024/687028734322147344/1273204543739396116 **Ⓜcadence [they]**:"
|
||||
+ " <https://www.youtube.com/watch?v=uX32idb1jMw>"
|
||||
+ `\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`,
|
||||
+ `\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`,
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
|
|
@ -4895,17 +4747,17 @@ test("event2message: stickers work", async t => {
|
|||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "[get_real2](https://bridge.example.org/download/sticker/cadence.moe/NyMXQFAAdniImbHzsygScbmN/_.webp)",
|
||||
content: "",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/cadence.moe/azCAhThKTojXSZJRoWwZmhvU",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
attachments: [{id: "0", filename: "get_real2.gif"}],
|
||||
pendingFiles: [{name: "get_real2.gif", mxc: "mxc://cadence.moe/NyMXQFAAdniImbHzsygScbmN"}]
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: stickers fetch mimetype from server when mimetype not provided", async t => {
|
||||
let called = 0
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.sticker",
|
||||
|
|
@ -4916,6 +4768,20 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
|
|||
},
|
||||
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}, {}, {}, {
|
||||
api: {
|
||||
async getMedia(mxc, options) {
|
||||
called++
|
||||
t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf")
|
||||
t.equal(options.method, "HEAD")
|
||||
return {
|
||||
status: 200,
|
||||
headers: new Map([
|
||||
["content-type", "image/gif"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
|
|
@ -4923,14 +4789,48 @@ test("event2message: stickers fetch mimetype from server when mimetype not provi
|
|||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "cadence [they]",
|
||||
content: "[YESYESYES](https://bridge.example.org/download/sticker/cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf/_.webp)",
|
||||
content: "",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
attachments: [{id: "0", filename: "YESYESYES.gif"}],
|
||||
pendingFiles: [{name: "YESYESYES.gif", mxc: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJf"}]
|
||||
}]
|
||||
}
|
||||
)
|
||||
t.equal(called, 1, "sticker headers should be fetched")
|
||||
})
|
||||
|
||||
test("event2message: stickers with unknown mimetype are not allowed", async t => {
|
||||
let called = 0
|
||||
try {
|
||||
await eventToMessage({
|
||||
type: "m.sticker",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
body: "something",
|
||||
url: "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe"
|
||||
},
|
||||
event_id: "$mL-eEVWCwOvFtoOiivDP7gepvf-fTYH6_ioK82bWDI0",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}, {}, {}, {
|
||||
api: {
|
||||
async getMedia(mxc, options) {
|
||||
called++
|
||||
t.equal(mxc, "mxc://cadence.moe/ybOWQCaXysnyUGuUCaQlTGJe")
|
||||
t.equal(options.method, "HEAD")
|
||||
return {
|
||||
status: 404,
|
||||
headers: new Map([
|
||||
["content-type", "application/json"]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
/* c8 ignore next */
|
||||
t.fail("should throw an error")
|
||||
} catch (e) {
|
||||
t.match(e.toString(), "mimetype")
|
||||
}
|
||||
})
|
||||
|
||||
test("event2message: static emojis work", async t => {
|
||||
|
|
@ -5558,141 +5458,6 @@ test("event2message: known and unknown emojis in the end are used for sprite she
|
|||
)
|
||||
})
|
||||
|
||||
test("event2message: com.beeper.per_message_profile overrides displayname and avatar_url", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello from unstable profile",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "custom-id",
|
||||
displayname: "Unstable Name",
|
||||
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo"
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Unstable Name",
|
||||
content: "hello from unstable profile",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: com.beeper.per_message_profile empty avatar_url clears avatar", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "hello with cleared avatar",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "no-avatar",
|
||||
displayname: "No Avatar User",
|
||||
avatar_url: ""
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "No Avatar User",
|
||||
content: "hello with cleared avatar",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: data-mx-profile-fallback element is stripped from formatted_body when per-message profile is present", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Tidus Herboren: one more test",
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: "<strong data-mx-profile-fallback>Tidus Herboren: </strong>one more test",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "tidus",
|
||||
displayname: "Tidus Herboren",
|
||||
avatar_url: "mxc://maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
has_fallback: true
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Tidus Herboren",
|
||||
content: "one more test",
|
||||
avatar_url: "https://bridge.example.org/download/matrix/maunium.net/hgXsKqlmRfpKvCZdUoWDkFQo",
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: displayname prefix is stripped from plain body when per-message profile has_fallback", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
type: "m.room.message",
|
||||
sender: "@cadence:cadence.moe",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Tidus Herboren: one more test",
|
||||
"com.beeper.per_message_profile": {
|
||||
id: "tidus",
|
||||
displayname: "Tidus Herboren",
|
||||
has_fallback: true
|
||||
}
|
||||
},
|
||||
event_id: "$g07oYSZFWBkxohNEfywldwgcWj1hbhDzQ1sBAKvqOOU",
|
||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe"
|
||||
}),
|
||||
{
|
||||
ensureJoined: [],
|
||||
messagesToDelete: [],
|
||||
messagesToEdit: [],
|
||||
messagesToSend: [{
|
||||
username: "Tidus Herboren",
|
||||
content: "one more test",
|
||||
avatar_url: undefined,
|
||||
allowed_mentions: {
|
||||
parse: ["users", "roles"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test("event2message: all unknown chess emojis are used for sprite sheet", async t => {
|
||||
t.deepEqual(
|
||||
await eventToMessage({
|
||||
|
|
|
|||
|
|
@ -413,7 +413,6 @@ async event => {
|
|||
console.error(e)
|
||||
return await api.leaveRoomWithReason(event.room_id, `I wasn't able to find out what this room is. Please report this as a bug. Check console for more details. (${e.toString()})`)
|
||||
}
|
||||
if (inviteRoomState?.encryption) return await api.leaveRoomWithReason(event.room_id, "Encrypted rooms are not supported for bridging. Please use an unencrypted room.")
|
||||
if (!inviteRoomState?.name) return await api.leaveRoomWithReason(event.room_id, `Please only invite me to rooms that have a name/avatar set. Update the room details and reinvite.`)
|
||||
await api.joinRoom(event.room_id)
|
||||
db.prepare("REPLACE INTO invite (mxid, room_id, type, name, topic, avatar) VALUES (?, ?, ?, ?, ?, ?)").run(event.sender, event.room_id, inviteRoomState.type, inviteRoomState.name, inviteRoomState.topic, inviteRoomState.avatar)
|
||||
|
|
@ -423,10 +422,7 @@ async event => {
|
|||
|
||||
if (event.content.membership === "leave" || event.content.membership === "ban") {
|
||||
// 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)
|
||||
// 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
|
||||
if (event.state_key === utils.bot) {
|
||||
|
|
@ -487,20 +483,6 @@ async event => {
|
|||
await roomUpgrade.onTombstone(event, api)
|
||||
}))
|
||||
|
||||
sync.addTemporaryListener(as, "type:m.room.encryption", guard("m.room.encryption",
|
||||
/**
|
||||
* @param {Ty.Event.StateOuter<Ty.Event.M_Room_Encryption>} event
|
||||
*/
|
||||
async event => {
|
||||
// Dramatically unbridge rooms if they become encrypted
|
||||
if (event.state_key !== "") return
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
if (!channelID) return
|
||||
const channel = discord.channels.get(channelID)
|
||||
if (!channel) return
|
||||
await createRoom.unbridgeChannel(channel, channel["guild_id"], "Encrypted rooms are not supported. This room was removed from the bridge.")
|
||||
}))
|
||||
|
||||
module.exports.stringifyErrorStack = stringifyErrorStack
|
||||
module.exports.sendError = sendError
|
||||
module.exports.printError = printError
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ function getStateEventOuter(roomID, type, key) {
|
|||
/**
|
||||
* @param {string} roomID
|
||||
* @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event]
|
||||
* @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?, encryption: string?}>}
|
||||
* @returns {Promise<{name: string?, topic: string?, avatar: string?, type: string?}>}
|
||||
*/
|
||||
async function getInviteState(roomID, event) {
|
||||
function getFromInviteRoomState(strippedState, nskey, key) {
|
||||
|
|
@ -191,8 +191,7 @@ async function getInviteState(roomID, event) {
|
|||
name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"),
|
||||
topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"),
|
||||
avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"),
|
||||
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"),
|
||||
encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm")
|
||||
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -228,8 +227,7 @@ async function getInviteState(roomID, event) {
|
|||
name: getFromInviteRoomState(strippedState, "m.room.name", "name"),
|
||||
topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"),
|
||||
avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"),
|
||||
type: getFromInviteRoomState(strippedState, "m.room.create", "type"),
|
||||
encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm")
|
||||
type: getFromInviteRoomState(strippedState, "m.room.create", "type")
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
|
@ -242,8 +240,7 @@ async function getInviteState(roomID, event) {
|
|||
name: room.name ?? null,
|
||||
topic: room.topic ?? null,
|
||||
avatar: room.avatar_url ?? null,
|
||||
type: room.room_type ?? null,
|
||||
encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null
|
||||
type: room.room_type ?? null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ async function _actuallyUploadDiscordFileToMxc(url) {
|
|||
writeRegistration(reg)
|
||||
return root
|
||||
}
|
||||
e.uploadURL = url
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const Ty = require("../types")
|
||||
const {pipeline} = require("stream").promises
|
||||
const sharp = require("sharp")
|
||||
|
|
@ -105,8 +104,7 @@ const commands = [{
|
|||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||
let matrixOnlyReason = null
|
||||
const matrixOnlyConclusion = "So the emoji will be uploaded on Matrix-side only. It will still be usable over the bridge, but may have degraded functionality."
|
||||
// Check if we can/should upload to Discord, for various causes
|
||||
|
|
@ -116,7 +114,7 @@ const commands = [{
|
|||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const slots = getSlotCount(guild.premium_tier)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
if (guild.emojis.length >= slots) {
|
||||
matrixOnlyReason = "CAPACITY"
|
||||
} else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
|
||||
|
|
@ -241,8 +239,7 @@ const commands = [{
|
|||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||
if (!guildID) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
|
|
@ -253,7 +250,7 @@ const commands = [{
|
|||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
|
|
@ -265,59 +262,6 @@ const commands = [{
|
|||
await discord.snow.channel.createThreadWithoutMessage(channelID, {type: 11, name: words.slice(1).join(" ")})
|
||||
}
|
||||
)
|
||||
}, {
|
||||
aliases: ["invite"],
|
||||
execute: replyctx(
|
||||
async (event, realBody, words, ctx) => {
|
||||
// Guard
|
||||
/** @type {string} */ // @ts-ignore
|
||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||
const channel = discord.channels.get(channelID)
|
||||
const guildID = channel?.["guild_id"]
|
||||
if (!guildID) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "This room isn't bridged to the other side."
|
||||
})
|
||||
}
|
||||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
||||
if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "This command creates an invite link to the Discord side. But you aren't allowed to do this, because if you were a Discord user, you wouldn't have the Create Invite permission."
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
var invite = await discord.snow.channel.createChannelInvite(channelID)
|
||||
} catch (e) {
|
||||
if (e.message === `{"message": "Missing Permissions", "code": 50013}`) {
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: "I don't have permission to create invites to the Discord channel/server."
|
||||
})
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
const validHours = Math.ceil(invite.max_age / (60 * 60))
|
||||
const validUses =
|
||||
( invite.max_uses === 0 ? "unlimited uses"
|
||||
: invite.max_uses === 1 ? "single-use"
|
||||
: `${invite.max_uses} uses`)
|
||||
return api.sendEvent(event.room_id, "m.room.message", {
|
||||
...ctx,
|
||||
msgtype: "m.text",
|
||||
body: `https://discord.gg/${invite.code}\nValid for next ${validHours} hours, ${validUses}.`
|
||||
})
|
||||
}
|
||||
)
|
||||
}]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -78,15 +78,6 @@ function readRegistration() {
|
|||
/** @type {import("../types").AppServiceRegistrationConfig} */ // @ts-ignore
|
||||
let reg = readRegistration()
|
||||
|
||||
if (reg) {
|
||||
fs.watch(registrationFilePath, {persistent: false}, () => {
|
||||
let newReg = readRegistration()
|
||||
if (newReg) {
|
||||
Object.assign(reg, newReg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.registrationFilePath = registrationFilePath
|
||||
module.exports.readRegistration = readRegistration
|
||||
module.exports.getTemplateRegistration = getTemplateRegistration
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration")
|
||||
|
||||
|
|
|
|||
|
|
@ -54,17 +54,17 @@ async function onBotMembership(event, api, createRoom) {
|
|||
assert.equal(event.type, "m.room.member")
|
||||
assert.equal(event.state_key, utils.bot)
|
||||
|
||||
// Check if an upgrade is pending for this room
|
||||
const newRoomID = event.room_id
|
||||
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
||||
if (!oldRoomID) return false
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return false
|
||||
|
||||
return await roomUpgradeSema.request(async () => {
|
||||
// Check if an upgrade is pending for this room
|
||||
const newRoomID = event.room_id
|
||||
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
||||
if (!oldRoomID) return false
|
||||
const channelRow = from("channel_room").join("guild_space", "guild_id").where({room_id: oldRoomID}).select("space_id", "guild_id", "channel_id").get()
|
||||
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||
|
||||
// Check if is join/invite
|
||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return false
|
||||
|
||||
// If invited, join
|
||||
if (event.content.membership === "invite") {
|
||||
await api.joinRoom(newRoomID)
|
||||
|
|
|
|||
|
|
@ -225,6 +225,19 @@ async function getViaServersQuery(roomID, api) {
|
|||
return qs
|
||||
}
|
||||
|
||||
function generatePermittedMediaHash(mxc) {
|
||||
assert(hasher, "xxhash is not ready yet")
|
||||
const mediaParts = mxc?.match(/^mxc:\/\/([^/]+)\/(\w+)$/)
|
||||
if (!mediaParts) return undefined
|
||||
|
||||
const serverAndMediaID = `${mediaParts[1]}/${mediaParts[2]}`
|
||||
const unsignedHash = hasher.h64(serverAndMediaID)
|
||||
const signedHash = unsignedHash - 0x8000000000000000n // shifting down to signed 64-bit range
|
||||
db.prepare("INSERT OR IGNORE INTO media_proxy (permitted_hash) VALUES (?)").run(signedHash)
|
||||
|
||||
return serverAndMediaID
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the introduction of authenticated media, this can no longer just be the /_matrix/media/r0/download URL
|
||||
* because Discord and Discord users cannot use those URLs. Media now has to be proxied through the bridge.
|
||||
|
|
|
|||
18
src/stdin.js
18
src/stdin.js
|
|
@ -23,26 +23,10 @@ const setPresence = sync.require("./d2m/actions/set-presence")
|
|||
const channelWebhook = sync.require("./m2d/actions/channel-webhook")
|
||||
const guildID = "112760669178241024"
|
||||
|
||||
async function ping() {
|
||||
const result = await api.ping().catch(e => ({ok: false, status: "net", root: e.message}))
|
||||
if (result.ok) {
|
||||
return "Ping OK. The homeserver and OOYE are talking to each other fine."
|
||||
} else {
|
||||
if (typeof result.root === "string") {
|
||||
var msg = `Cannot reach homeserver: ${result.root}`
|
||||
} else if (result.root.error) {
|
||||
var msg = `Homeserver said: [${result.status}] ${result.root.error}`
|
||||
} else {
|
||||
var msg = `Homeserver said: [${result.status}] ${JSON.stringify(result.root)}`
|
||||
}
|
||||
return msg + "\nMatrix->Discord won't work until you fix this.\nIf your installation has recently changed, consider `npm run setup` again."
|
||||
}
|
||||
}
|
||||
|
||||
if (process.stdin.isTTY) {
|
||||
setImmediate(() => {
|
||||
if (!passthrough.repl) {
|
||||
const cli = repl.start({prompt: "", eval: customEval, writer: s => s})
|
||||
const cli = repl.start({ prompt: "", eval: customEval, writer: s => s })
|
||||
Object.assign(cli.context, passthrough)
|
||||
passthrough.repl = cli
|
||||
}
|
||||
|
|
|
|||
9
src/types.d.ts
vendored
9
src/types.d.ts
vendored
|
|
@ -157,7 +157,7 @@ export namespace Event {
|
|||
type: string
|
||||
state_key: string
|
||||
sender: string
|
||||
content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias | Event.M_Room_Encryption
|
||||
content: Event.M_Room_Create | Event.M_Room_Name | Event.M_Room_Avatar | Event.M_Room_Topic | Event.M_Room_JoinRules | Event.M_Room_CanonicalAlias
|
||||
}
|
||||
|
||||
export type M_Room_Create = {
|
||||
|
|
@ -390,12 +390,6 @@ export namespace Event {
|
|||
body: string
|
||||
replacement_room: string
|
||||
}
|
||||
|
||||
export type M_Room_Encryption = {
|
||||
algorithm: string
|
||||
rotation_period_ms?: number
|
||||
rotation_period_msgs?: number
|
||||
}
|
||||
}
|
||||
|
||||
export namespace R {
|
||||
|
|
@ -443,7 +437,6 @@ export namespace R {
|
|||
num_joined_members: number
|
||||
room_id: string
|
||||
room_type?: string
|
||||
encryption?: string
|
||||
}
|
||||
|
||||
export type ResolvedRoom = {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ function renderPath(event, path, locals) {
|
|||
compile()
|
||||
fs.watch(path, {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "includes"), {persistent: false}, compile)
|
||||
fs.watch(join(__dirname, "pug", "fragments"), {persistent: false}, compile)
|
||||
}
|
||||
|
||||
const cb = pugCache.get(path)
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
//- locals: guild, guild_id
|
||||
|
||||
include ../includes/default-roles-list.pug
|
||||
+default-roles-list(guild, guild_id)
|
||||
+add-roles-menu(guild, guild_id)
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
extends includes/template.pug
|
||||
include includes/default-roles-list.pug
|
||||
|
||||
mixin badge-readonly
|
||||
.s-badge.s-badge__xs.s-badge__icon.s-badge__muted
|
||||
|
|
@ -77,7 +76,7 @@ block body
|
|||
|
||||
if space_id
|
||||
h2.mt48.fs-headline1 Server settings
|
||||
h3.mt32.fs-category How Matrix users join
|
||||
h3.mt32.fs-category Privacy level
|
||||
span#privacy-level-loading
|
||||
.s-card
|
||||
form(hx-post=rel("/api/privacy-level") hx-trigger="change" hx-indicator="#privacy-level-loading" hx-disabled-elt="input")
|
||||
|
|
@ -106,24 +105,6 @@ block body
|
|||
p.s-description.m0 Shareable invite links, like Discord
|
||||
p.s-description.m0 Publicly listed in directory, like Discord server discovery
|
||||
|
||||
h3.mt32.fs-category Default roles
|
||||
.s-card
|
||||
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") hx-sync="this:drop" hx-indicator="#add-role-loading" hx-target="#default-roles-list" hx-select="#default-roles-list" hx-swap="outerHTML")#default-roles
|
||||
input(type="hidden" name="guild_id" value=guild_id)
|
||||
.d-flex.fw-wrap.g4
|
||||
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
||||
|
||||
+default-roles-list(guild, guild_id)
|
||||
|
||||
button(type="button" popovertarget="role-add").s-btn__dropdown.s-tag.s-tag__md.fs-body1.p0
|
||||
.s-tag--dismiss.m1
|
||||
!= icons.Icons.IconPlusSm
|
||||
|
||||
#role-add.s-popover(popover style="display: revert").ws2.px0.py4.bs-lg.overflow-visible
|
||||
.s-popover--arrow.s-popover--arrow__tc
|
||||
+add-roles-menu(guild, guild_id)
|
||||
p.fc-medium.mb0.mt8 Matrix users will start with these roles. If your main channels are gated by a role, use this to let Matrix users skip the gate.
|
||||
|
||||
h3.mt32.fs-category Features
|
||||
.s-card.d-grid.px0.g16
|
||||
form.d-flex.ai-center.g16
|
||||
|
|
@ -249,11 +230,6 @@ block body
|
|||
ul.my8.ml24
|
||||
each row in removedLinkedRooms
|
||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||
h3.mt24 Unavailable rooms: Encryption not supported
|
||||
.s-card.p0
|
||||
ul.my8.ml24
|
||||
each row in removedEncryptedRooms
|
||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
||||
h3.mt24 Unavailable rooms: Wrong type
|
||||
.s-card.p0
|
||||
ul.my8.ml24
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
mixin default-roles-list(guild, guild_id)
|
||||
#default-roles-list(style="display: contents")
|
||||
each roleID in select("role_default", "role_id", {guild_id}).pluck().all()
|
||||
- let r = guild.roles.find(r => r.id === roleID)
|
||||
if r
|
||||
.s-tag.s-tag__md.fs-body1= r.name
|
||||
span(id=`role-loading-${roleID}`)
|
||||
button(name="remove_role" value=roleID hx-post="api/default-roles" hx-trigger="click consume" hx-indicator=`#role-loading-${roleID}`).s-tag--dismiss
|
||||
!= icons.Icons.IconClearSm
|
||||
|
||||
mixin add-roles-menu(guild, guild_id)
|
||||
ul.s-menu(role="menu" hx-swap-oob="true").overflow-y-auto.overflow-x-hidden#add-roles-menu
|
||||
li.s-menu--title.d-flex(role="separator") Select role
|
||||
span#add-role-loading
|
||||
each r in guild.roles.sort((a, b) => b.position - a.position)
|
||||
if r.id !== guild_id && !r.managed
|
||||
- let selected = !!select("role_default", "role_id", {guild_id, role_id: r.id}).get()
|
||||
li(role="menuitem")
|
||||
button(name="toggle_role" value=r.id class={"is-selected": selected}).s-block-link.s-block-link__left= r.name
|
||||
|
|
@ -88,28 +88,9 @@ html(lang="en")
|
|||
--_ts-multiple-bg: var(--green-400);
|
||||
--_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) {
|
||||
background-color: var(--theme-topbar-item-background-hover, var(--black-200)) !important;
|
||||
}
|
||||
.s-btn__dropdown.s-tag:has(+ :popover-open) .s-tag--dismiss {
|
||||
background-color: var(--black-500) !important;
|
||||
color: var(--black-150) !important;
|
||||
}
|
||||
.s-tag .is-loading {
|
||||
margin-right: -4px;
|
||||
}
|
||||
.s-tag .is-loading + .s-tag--dismiss {
|
||||
display: none !important;
|
||||
}
|
||||
a.s-block-link, .s-block-link {
|
||||
--_bl-bs-color: var(--green-400);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.theme-system .s-popover {
|
||||
--_po-bg: var(--black-100);
|
||||
|
|
@ -160,15 +141,11 @@ html(lang="en")
|
|||
//- Guild list popover
|
||||
script.
|
||||
document.querySelectorAll("[popovertarget]").forEach(e => {
|
||||
const target = document.getElementById(e.getAttribute("popovertarget"))
|
||||
e.addEventListener("click", calculate)
|
||||
target.addEventListener("toggle", calculate)
|
||||
function calculate() {
|
||||
const buttonRect = e.getBoundingClientRect()
|
||||
const targetRect = target.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(buttonRect.bottom + window.scrollY)}px; left: ${Math.floor(Math.max(targetRect.width / 2, buttonRect.left + buttonRect.width / 2))}px; width: ${Math.floor(buttonRect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
e.addEventListener("click", () => {
|
||||
const rect = e.getBoundingClientRect()
|
||||
const t = `:popover-open { position: absolute; top: ${Math.floor(rect.bottom)}px; left: ${Math.floor(rect.left + rect.width / 2)}px; width: ${Math.floor(rect.width)}px; transform: translateX(-50%); box-sizing: content-box; margin: 0 }`
|
||||
document.styleSheets[0].insertRule(t, document.styleSheets[0].cssRules.length)
|
||||
}
|
||||
})
|
||||
})
|
||||
//- Prevent default
|
||||
script.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
const {_cache} = require("./download-discord")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const fs = require("fs")
|
||||
const {convertImageStream} = require("../../m2d/converters/emoji-sheet")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
const streamWeb = require("stream/web")
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ const assert = require("assert/strict")
|
|||
const {z} = require("zod")
|
||||
const {defineEventHandler, createError, readValidatedBody, getRequestHeader, setResponseHeader, sendRedirect, H3Event} = require("h3")
|
||||
|
||||
const {as, db, sync, select, discord} = require("../../passthrough")
|
||||
const {as, db, sync, select} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../auth")} */
|
||||
const auth = sync.require("../auth")
|
||||
/** @type {import("../pug-sync")} */
|
||||
const pugSync = sync.require("../pug-sync")
|
||||
/** @type {import("../../d2m/actions/set-presence")} */
|
||||
const setPresence = sync.require("../../d2m/actions/set-presence")
|
||||
|
||||
|
|
@ -22,14 +20,6 @@ function getCreateSpace(event) {
|
|||
return event.context.createSpace || sync.require("../../d2m/actions/create-space")
|
||||
}
|
||||
|
||||
const schema = {
|
||||
defaultRoles: z.object({
|
||||
guild_id: z.string(),
|
||||
toggle_role: z.string().optional(),
|
||||
remove_role: z.string().optional()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef Options
|
||||
* @prop {(value: string?) => number} transform
|
||||
|
|
@ -104,39 +94,3 @@ as.router.post("/api/privacy-level", defineToggle("privacy_level", {
|
|||
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
|
||||
}
|
||||
}))
|
||||
|
||||
as.router.post("/api/default-roles", defineEventHandler(async event => {
|
||||
const parsedBody = await readValidatedBody(event, schema.defaultRoles.parse)
|
||||
|
||||
const managed = await auth.getManagedGuilds(event)
|
||||
const guildID = parsedBody.guild_id
|
||||
if (!managed.has(guildID)) throw createError({status: 403, message: "Forbidden", data: "Can't change settings for a guild you don't have Manage Server permissions in"})
|
||||
|
||||
const roleID = parsedBody.toggle_role || parsedBody.remove_role
|
||||
assert(roleID)
|
||||
assert.notEqual(guildID, roleID) // the @everyone role is always default
|
||||
|
||||
const guild = discord.guilds.get(guildID)
|
||||
assert(guild)
|
||||
|
||||
let shouldRemove = !!parsedBody.remove_role
|
||||
if (!shouldRemove) {
|
||||
shouldRemove = !!select("role_default", "role_id", {guild_id: guildID, role_id: roleID}).get()
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
db.prepare("DELETE FROM role_default WHERE guild_id = ? AND role_id = ?").run(guildID, roleID)
|
||||
} else {
|
||||
assert(guild.roles.find(r => r.id === roleID))
|
||||
db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, roleID)
|
||||
}
|
||||
|
||||
const createSpace = getCreateSpace(event)
|
||||
await createSpace.syncSpaceFully(guildID) // this is inefficient but OK to call infrequently on user request
|
||||
|
||||
if (getRequestHeader(event, "HX-Request")) {
|
||||
return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
|
||||
} else {
|
||||
return sendRedirect(event, `/guild?guild_id=${guildID}`, 302)
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {select} = require("../../passthrough")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
|
|
|
|||
|
|
@ -123,14 +123,13 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
|||
let unlinkedRooms = [...rooms]
|
||||
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
||||
let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type)
|
||||
let removedEncryptedRooms = dUtils.filterTo(unlinkedRooms, r => !r.encryption && !r["im.nheko.summary.encryption"])
|
||||
// https://discord.com/developers/docs/topics/threads#active-archived-threads
|
||||
// need to filter out linked archived threads from unlinkedRooms, will just do that by comparing against the name
|
||||
let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/))
|
||||
|
||||
return {
|
||||
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms
|
||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {_getPosition} = require("./guild")
|
||||
|
|
|
|||
|
|
@ -204,12 +204,6 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
|||
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
|
||||
}
|
||||
|
||||
// Check room is not encrypted
|
||||
const encryption = await api.getStateEvent(parsedBody.matrix, "m.room.encryption", "").catch(() => null)
|
||||
if (encryption) {
|
||||
throw createError({status: 400, message: "Bad Request", data: "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room."})
|
||||
}
|
||||
|
||||
// Check bridge has PL 100
|
||||
const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api)
|
||||
if (selfPowerLevel < (powerLevels?.state_default ?? 50) || selfPowerLevel < 100) throw createError({status: 400, message: "Bad Request", data: "OOYE needs power level 100 (admin) in the target Matrix room"})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
const {select, db} = require("../../passthrough")
|
||||
|
|
@ -435,47 +435,6 @@ test("web link room: check that bridge can join room (uses via for join attempt)
|
|||
t.equal(called, 2)
|
||||
})
|
||||
|
||||
test("web link room: check that room is not encrypted", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
|
||||
sessionData: {
|
||||
managedGuilds: ["665289423482519565"]
|
||||
},
|
||||
body: {
|
||||
discord: "665310973967597573",
|
||||
matrix: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
guild_id: "665289423482519565"
|
||||
},
|
||||
api: {
|
||||
async joinRoom(roomID) {
|
||||
called++
|
||||
return roomID
|
||||
},
|
||||
async *generateFullHierarchy(spaceID) {
|
||||
called++
|
||||
t.equal(spaceID, "!zTMspHVUBhFLLSdmnS:cadence.moe")
|
||||
yield {
|
||||
room_id: "!NDbIqNpJyPvfKRnNcr:cadence.moe",
|
||||
children_state: [],
|
||||
guest_can_join: false,
|
||||
num_joined_members: 2
|
||||
}
|
||||
/* c8 ignore next */
|
||||
},
|
||||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
if (type === "m.room.encryption" && key === "") {
|
||||
return {algorithm: "m.megolm.v1.aes-sha2"}
|
||||
}
|
||||
throw new Error("Unknown state event")
|
||||
}
|
||||
}
|
||||
}))
|
||||
t.equal(error.data, "Encrypted rooms are not supported for bridging. Please replace it with an unencrypted room.")
|
||||
t.equal(called, 3)
|
||||
})
|
||||
|
||||
test("web link room: check that bridge has PL 100 in target room", async t => {
|
||||
let called = 0
|
||||
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
|
||||
|
|
@ -506,10 +465,9 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
|
|||
async getStateEvent(roomID, type, key) {
|
||||
called++
|
||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||
if (type === "m.room.power_levels" && key === "") {
|
||||
return {users_default: 50}
|
||||
}
|
||||
throw new Error("Unknown state event")
|
||||
t.equal(type, "m.room.power_levels")
|
||||
t.equal(key, "")
|
||||
return {users_default: 50}
|
||||
},
|
||||
async getStateEventOuter(roomID, type, key) {
|
||||
called++
|
||||
|
|
@ -531,7 +489,7 @@ test("web link room: check that bridge has PL 100 in target room", async t => {
|
|||
}
|
||||
}))
|
||||
t.equal(error.data, "OOYE needs power level 100 (admin) in the target Matrix room")
|
||||
t.equal(called, 5)
|
||||
t.equal(called, 4)
|
||||
})
|
||||
|
||||
test("web link room: successfully calls createRoom", async t => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {router, test} = require("../../../test/web")
|
||||
const {MatrixServerError} = require("../../matrix/mreq")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const assert = require("assert/strict")
|
||||
const {router, test} = require("../../../test/web")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {tryToCatch} = require("try-to-catch")
|
||||
const tryToCatch = require("try-to-catch")
|
||||
const {test} = require("supertape")
|
||||
const {router} = require("../../../test/web")
|
||||
|
||||
|
|
|
|||
|
|
@ -83,13 +83,7 @@ function tryStatic(event, fallthrough) {
|
|||
// Everything else
|
||||
else {
|
||||
const mime = mimeTypes.lookup(id)
|
||||
if (typeof mime === "string") {
|
||||
if (mime.startsWith("text/")) {
|
||||
defaultContentType(event, mime + "; charset=utf-8") // usually wise
|
||||
} else {
|
||||
defaultContentType(event, mime)
|
||||
}
|
||||
}
|
||||
if (typeof mime === "string") defaultContentType(event, mime)
|
||||
return {
|
||||
size: stats.size
|
||||
}
|
||||
|
|
@ -100,7 +94,7 @@ function tryStatic(event, fallthrough) {
|
|||
const path = join(publicDir, id)
|
||||
return pugSync.renderPath(event, path, {})
|
||||
} else {
|
||||
return fs.createReadStream(join(publicDir, id))
|
||||
return fs.promises.readFile(join(publicDir, id))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
262
test/data.js
262
test/data.js
|
|
@ -19,26 +19,6 @@ module.exports = {
|
|||
default_thread_rate_limit_per_user: 0,
|
||||
guild_id: "112760669178241024"
|
||||
},
|
||||
voice: {
|
||||
voice_background_display: null,
|
||||
version: 1774469910848,
|
||||
user_limit: 0,
|
||||
type: 2,
|
||||
theme_color: null,
|
||||
status: null,
|
||||
rtc_region: null,
|
||||
rate_limit_per_user: 0,
|
||||
position: 0,
|
||||
permission_overwrites: [],
|
||||
parent_id: "805261291908104252",
|
||||
nsfw: false,
|
||||
name: "🍞丨[8user] Piece",
|
||||
last_message_id: "1459912691098325137",
|
||||
id: "1036840786093953084",
|
||||
flags: 0,
|
||||
bitrate: 256000,
|
||||
guild_id: "112760669178241024"
|
||||
},
|
||||
updates: {
|
||||
type: 0,
|
||||
topic: "Updates and release announcements for Out Of Your Element.",
|
||||
|
|
@ -2035,80 +2015,6 @@ module.exports = {
|
|||
tts: false
|
||||
}
|
||||
},
|
||||
reply_to_member_join: {
|
||||
type: 19,
|
||||
content: "when the broke friend who we pay to bring food shows up at the medieval lord party",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-30T12:11:04.443000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1488148556962332692",
|
||||
channel_id: "475599038536744962",
|
||||
author: {
|
||||
id: "576945009408999426",
|
||||
username: "randomllama121",
|
||||
avatar: "08510a70f957106dad1580323c40cd7a",
|
||||
discriminator: "0",
|
||||
public_flags: 128,
|
||||
flags: 128,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "random :3",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 0,
|
||||
channel_id: "475599038536744962",
|
||||
message_id: "1488146734352826478",
|
||||
guild_id: "475599038536744960"
|
||||
},
|
||||
referenced_message: {
|
||||
type: 7,
|
||||
content: "",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-30T12:03:49.899000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1488146734352826478",
|
||||
channel_id: "475599038536744962",
|
||||
author: {
|
||||
id: "1461677775554478161",
|
||||
username: "peasant321_76775",
|
||||
avatar: null,
|
||||
discriminator: "0",
|
||||
public_flags: 0,
|
||||
flags: 0,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "PEASANT!!",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: null,
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false
|
||||
}
|
||||
},
|
||||
attachment_no_content: {
|
||||
id: "1124628646670389348",
|
||||
type: 0,
|
||||
|
|
@ -4711,7 +4617,7 @@ module.exports = {
|
|||
flags: 0,
|
||||
components: []
|
||||
},
|
||||
extreme_html_escaping: {
|
||||
escaping_crazy_html_tags: {
|
||||
id: "1158894131322552391",
|
||||
type: 0,
|
||||
content: "",
|
||||
|
|
@ -5161,141 +5067,6 @@ module.exports = {
|
|||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false
|
||||
},
|
||||
four_images: {
|
||||
type: 0,
|
||||
content: "",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-12T18:00:50.737000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 16384,
|
||||
components: [],
|
||||
id: "1481713598278533241",
|
||||
channel_id: "687028734322147344",
|
||||
author: {
|
||||
id: "112760500130975744",
|
||||
username: "minimus",
|
||||
avatar: "a_a354b9eaff512485b49c82b13691b941",
|
||||
discriminator: "0",
|
||||
public_flags: 512,
|
||||
flags: 512,
|
||||
banner: null,
|
||||
accent_color: null,
|
||||
global_name: "minimus",
|
||||
avatar_decoration_data: null,
|
||||
collectibles: null,
|
||||
display_name_styles: { font_id: 11, effect_id: 5, colors: [ 6106655 ] },
|
||||
banner_color: null,
|
||||
clan: null,
|
||||
primary_guild: null
|
||||
},
|
||||
pinned: false,
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 1,
|
||||
channel_id: "637339857118822430",
|
||||
message_id: "1481696763483258891",
|
||||
guild_id: "408573045540651009"
|
||||
},
|
||||
message_snapshots: [
|
||||
{
|
||||
message: {
|
||||
type: 0,
|
||||
content: "https://fixupx.com/i/status/2032003668787020046",
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
attachments: [],
|
||||
embeds: [
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
description: "4chan owner Hiroyuki, Evangelion director Hideaki Anno and GACKT to participate in “humanity’s last non\\-AI made social network”\n" +
|
||||
"︀︀\n" +
|
||||
"︀︀[automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/](https://automaton-media.com/en/news/4chan-owner-hiroyuki-evangelion-director-hideaki-anno-and-gackt-to-participate-in-humanitys-last-non-ai-made-social-network/)\n" +
|
||||
"\n" +
|
||||
"**[💬](https://x.com/intent/tweet?in_reply_to=2032003668787020046) 36 [🔁](https://x.com/intent/retweet?tweet_id=2032003668787020046) 212 [❤](https://x.com/intent/like?tweet_id=2032003668787020046) 3\\.0K 👁 131\\.7K **",
|
||||
color: 6513919,
|
||||
timestamp: "2026-03-12T08:00:02+00:00",
|
||||
author: {
|
||||
name: "AUTOMATON WEST (@AUTOMATON_ENG)",
|
||||
url: "https://x.com/AUTOMATON_ENG/status/2032003668787020046",
|
||||
icon_url: "https://pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg",
|
||||
proxy_icon_url: "https://images-ext-1.discordapp.net/external/1OzGhjvZTRstTxM38_7pqHXlmdbMddqh1F8R0-WrKqw/https/pbs.twimg.com/profile_images/1353559126693961729/pz-WVnDc_200x200.jpg"
|
||||
},
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/NkNgp2SyY1OCH9IdS8hqsUqbnbrp3A9oLNwYusVVCVQ/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUyf6bQAM3yts.jpg",
|
||||
width: 872,
|
||||
height: 886,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "6vcFFwL6R3lye2V3l1mIl5l3WPN5FZ8H",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
footer: {
|
||||
text: "FixupX",
|
||||
icon_url: "https://assets.fxembed.com/logos/fixupx64.png",
|
||||
proxy_icon_url: "https://images-ext-1.discordapp.net/external/LwQ70Uiqfu0OCN4ZbA4f482TGCgQa-xGsnUFYfhIgYA/https/assets.fxembed.com/logos/fixupx64.png"
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/Rquh1ec-tG9hMqdHqIVSphO7zf5B5Fg_7yTWhCjlsek/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUgxybQAE4FtJ.jpg",
|
||||
width: 1114,
|
||||
height: 991,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "JQgKDoL3epZ8ZIdnlmmHZ4d4CIGmUEc=",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUrPobgAAeb90.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/XrkhHNH3CvlZYvjkdykVnf-_xdz6HWX8uwesoAwwSfY/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUrPobgAAeb90.jpg",
|
||||
width: 944,
|
||||
height: 954,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "m/cJDwCbV0mfaoZzlihqeXdqCVN9A6oD",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
},
|
||||
{
|
||||
type: "rich",
|
||||
url: "https://fixupx.com/i/status/2032003668787020046",
|
||||
image: {
|
||||
url: "https://pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg?name=orig",
|
||||
proxy_url: "https://images-ext-1.discordapp.net/external/lO-5hBMU9bGH13Ax9xum2T2Mg0ATdv0b6BEx_VeVi80/%3Fname%3Dorig/https/pbs.twimg.com/media/HDMUuy5bgAAInj5.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
content_type: "image/jpeg",
|
||||
placeholder: "tfcJDIK3mIl1eIiPdY23dX9b9w==",
|
||||
placeholder_version: 1,
|
||||
flags: 0
|
||||
},
|
||||
content_scan_version: 4
|
||||
}
|
||||
],
|
||||
timestamp: "2026-03-12T16:53:57.009000+00:00",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
message_with_components: {
|
||||
|
|
@ -6264,37 +6035,6 @@ module.exports = {
|
|||
components: [],
|
||||
position: 12
|
||||
},
|
||||
channel_follow_add: {
|
||||
type: 12,
|
||||
content: "PluralKit #downtime",
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
timestamp: "2026-03-24T23:16:04.097Z",
|
||||
edited_timestamp: null,
|
||||
flags: 0,
|
||||
components: [],
|
||||
id: "1486141581047369888",
|
||||
channel_id: "1451125453082591314",
|
||||
author: {
|
||||
id: "154058479798059009",
|
||||
username: "exaptations",
|
||||
discriminator: "0",
|
||||
avatar: "57b5cfe09a48a5902f2eb8fa65bb1b80",
|
||||
bot: false,
|
||||
flags: 0,
|
||||
globalName: "Exa",
|
||||
},
|
||||
pinned: false,
|
||||
mentions: [],
|
||||
mention_roles: [],
|
||||
mention_everyone: false,
|
||||
tts: false,
|
||||
message_reference: {
|
||||
type: 0,
|
||||
channel_id: "1015204661701124206",
|
||||
guild_id: "466707357099884544"
|
||||
}
|
||||
},
|
||||
updated_to_start_thread_from_here: {
|
||||
t: "MESSAGE_UPDATE",
|
||||
s: 19,
|
||||
|
|
|
|||
|
|
@ -38,28 +38,15 @@ INSERT INTO sim (user_id, username, sim_name, mxid) VALUES
|
|||
('1109360903096369153', 'Amanda', 'amanda', '@_ooye_amanda:cadence.moe'),
|
||||
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
|
||||
('320067006521147393', 'papiophidian', 'papiophidian', '@_ooye_papiophidian: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');
|
||||
|
||||
('772659086046658620', 'cadence.worm', 'cadence', '@_ooye_cadence:cadence.moe');
|
||||
|
||||
INSERT INTO sim_member (mxid, room_id, hashed_profile_content) VALUES
|
||||
('@_ooye_bojack_horseman:cadence.moe', '!hYnGGlPHlbujVVfktC: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);
|
||||
('@_ooye_cadence:cadence.moe', '!BnKuBPCvyfOkhcUjEu:cadence.moe', NULL);
|
||||
|
||||
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
|
||||
('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)
|
||||
WITH a (message_id, channel_id) AS (VALUES
|
||||
('1106366167788044450', '122155380120748034'),
|
||||
|
|
|
|||
37
test/test.js
37
test/test.js
|
|
@ -6,29 +6,31 @@ const sqlite = require("better-sqlite3")
|
|||
const {Writable} = require("stream")
|
||||
const migrate = require("../src/db/migrate")
|
||||
const HeatSync = require("heatsync")
|
||||
const {test} = require("supertape")
|
||||
const {test, extend} = require("supertape")
|
||||
const data = require("./data")
|
||||
const {green} = require("ansi-colors")
|
||||
const mixin = require("@cloudrac3r/mixin-deep")
|
||||
|
||||
const passthrough = require("../src/passthrough")
|
||||
const db = new sqlite(":memory:")
|
||||
|
||||
const registration = require("../src/matrix/read-registration")
|
||||
registration.reg = mixin(registration.getTemplateRegistration("cadence.moe"), {
|
||||
id: "baby",
|
||||
url: "http://localhost:6693",
|
||||
as_token: "don't actually take authenticated actions on the server",
|
||||
hs_token: "don't actually take authenticated actions on the server",
|
||||
ooye: {
|
||||
server_origin: "https://matrix.cadence.moe",
|
||||
bridge_origin: "https://bridge.example.org",
|
||||
discord_token: "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby",
|
||||
discord_client_secret: "baby",
|
||||
web_password: "password123",
|
||||
time_zone: "Pacific/Auckland",
|
||||
}
|
||||
})
|
||||
const {reg} = require("../src/matrix/read-registration")
|
||||
reg.ooye.discord_token = "Njg0MjgwMTkyNTUzODQ0NzQ3.Xl3zlw.baby"
|
||||
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.namespace_prefix = "_ooye_"
|
||||
reg.sender_localpart = "_ooye_bot"
|
||||
reg.id = "baby"
|
||||
reg.as_token = "don't actually take authenticated actions on the server"
|
||||
reg.hs_token = "don't actually take authenticated actions on the server"
|
||||
reg.namespaces = {
|
||||
users: [{regex: "@_ooye_.*:cadence.moe", exclusive: true}],
|
||||
aliases: [{regex: "#_ooye_.*:cadence.moe", exclusive: true}]
|
||||
}
|
||||
reg.ooye.bridge_origin = "https://bridge.example.org"
|
||||
reg.ooye.time_zone = "Pacific/Auckland"
|
||||
reg.ooye.max_file_size = 5000000
|
||||
reg.ooye.web_password = "password123"
|
||||
reg.ooye.include_user_id_in_mxid = false
|
||||
|
||||
const sync = new HeatSync({watchFS: false})
|
||||
|
||||
|
|
@ -152,7 +154,6 @@ 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.pk")
|
||||
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/thread-to-announcement.test")
|
||||
require("../src/d2m/converters/user-to-mxid.test")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue