Compare commits
3 commits
3d3671e05a
...
c8b0f23db3
| Author | SHA1 | Date | |
|---|---|---|---|
| c8b0f23db3 | |||
| fcd4eb4e51 | |||
| 536ab56048 |
9 changed files with 156 additions and 84 deletions
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -10,7 +10,7 @@
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||||
"@cloudrac3r/discord-markdown": "^2.6.7",
|
"@cloudrac3r/discord-markdown": "^2.6.10",
|
||||||
"@cloudrac3r/giframe": "^0.4.3",
|
"@cloudrac3r/giframe": "^0.4.3",
|
||||||
"@cloudrac3r/html-template-tag": "^5.0.1",
|
"@cloudrac3r/html-template-tag": "^5.0.1",
|
||||||
"@cloudrac3r/in-your-element": "^1.1.1",
|
"@cloudrac3r/in-your-element": "^1.1.1",
|
||||||
|
|
@ -242,9 +242,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cloudrac3r/discord-markdown": {
|
"node_modules/@cloudrac3r/discord-markdown": {
|
||||||
"version": "2.6.8",
|
"version": "2.6.10",
|
||||||
"resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.8.tgz",
|
"resolved": "https://registry.npmjs.org/@cloudrac3r/discord-markdown/-/discord-markdown-2.6.10.tgz",
|
||||||
"integrity": "sha512-ZrSimHqmLqXR+W3U1n6ge6poAjmQaMzXyWrTkT36znrgKhfuQAYxLBtKTf7m+cmr3VlaDVM2P+iPdSeTeaM0qg==",
|
"integrity": "sha512-E+F9UYDUHP2kHDCciX63SBzgsUnHpu2Pp/h98x9Zo+vKuzXjCQ5PcFNdUlH6M18bvHDZPoIsKVmjnON8UYaAPQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"simple-markdown": "^0.7.3"
|
"simple-markdown": "^0.7.3"
|
||||||
|
|
@ -1227,26 +1227,15 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
|
||||||
"version": "15.7.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
|
||||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.55",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
|
||||||
"integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==",
|
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"csstype": "^3.2.2"
|
||||||
"@types/scheduler": "*",
|
|
||||||
"csstype": "^3.0.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/scheduler": {
|
|
||||||
"version": "0.16.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
|
||||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
|
|
||||||
},
|
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||||
|
|
@ -1668,9 +1657,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
|
|
@ -2825,6 +2815,7 @@
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/simple-markdown/-/simple-markdown-0.7.3.tgz",
|
||||||
"integrity": "sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==",
|
"integrity": "sha512-uGXIc13NGpqfPeFJIt/7SHHxd6HekEJYtsdoCM06mEBPL9fQH/pSD7LRM6PZ7CKchpSvxKL4tvwMamqAaNDAyg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react": ">=16.0.0"
|
"@types/react": ">=16.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||||
"@cloudrac3r/discord-markdown": "^2.6.7",
|
"@cloudrac3r/discord-markdown": "^2.6.10",
|
||||||
"@cloudrac3r/giframe": "^0.4.3",
|
"@cloudrac3r/giframe": "^0.4.3",
|
||||||
"@cloudrac3r/html-template-tag": "^5.0.1",
|
"@cloudrac3r/html-template-tag": "^5.0.1",
|
||||||
"@cloudrac3r/in-your-element": "^1.1.1",
|
"@cloudrac3r/in-your-element": "^1.1.1",
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --enable-source-maps start.js",
|
"start": "node --enable-source-maps start.js",
|
||||||
"setup": "node --enable-source-maps scripts/setup.js",
|
"setup": "node --enable-source-maps scripts/setup.js",
|
||||||
|
"build": "mkdir -p dist/out-of-your-element && cp -R src dist/out-of-your-element && cp -R docs dist/out-of-your-element && npx tsdown",
|
||||||
"addbot": "node addbot.js",
|
"addbot": "node addbot.js",
|
||||||
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
|
"test": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js | tap-dot",
|
||||||
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
"test-slow": "cross-env FORCE_COLOR=true supertape --no-check-assertions-count --format tap --no-worker test/test.js -- --slow | tap-dot",
|
||||||
|
|
|
||||||
|
|
@ -20,31 +20,39 @@ const emitter = new EventEmitter()
|
||||||
* (or before the it has finished being bridged to an event).
|
* (or before the it has finished being bridged to an event).
|
||||||
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
||||||
* @template {(...args: any[]) => Promise<any>} T
|
* @template {(...args: any[]) => Promise<any>} T
|
||||||
* @param {string} messageID
|
* @param {string} inputID
|
||||||
* @param {T} fn
|
* @param {T} fn
|
||||||
* @param {Parameters<T>} rest
|
* @param {Parameters<T>} rest
|
||||||
* @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered
|
* @returns {boolean} false if the event was found and the function will be ignored, true if the event was not found and the function will be retriggered
|
||||||
*/
|
*/
|
||||||
function eventNotFoundThenRetrigger(messageID, fn, ...rest) {
|
function eventNotFoundThenRetrigger(inputID, fn, ...rest) {
|
||||||
if (!paused.has(messageID)) {
|
if (!paused.has(inputID)) {
|
||||||
const eventID = select("event_message", "event_id", {message_id: messageID}).pluck().get()
|
if (inputID.match(/^[0-9]+$/)) {
|
||||||
|
const eventID = select("event_message", "event_id", {message_id: inputID}).pluck().get()
|
||||||
if (eventID) {
|
if (eventID) {
|
||||||
debugRetrigger(`[retrigger] OK mid <-> eid = ${messageID} <-> ${eventID}`)
|
debugRetrigger(`[retrigger] OK mid <-> eid = ${inputID} <-> ${eventID}`)
|
||||||
return false // event was found so don't retrigger
|
return false // event was found so don't retrigger
|
||||||
}
|
}
|
||||||
|
} else if (inputID.match(/^\$/)) {
|
||||||
|
const messageID = select("event_message", "message_id", {event_id: inputID}).pluck().get()
|
||||||
|
if (messageID) {
|
||||||
|
debugRetrigger(`[retrigger] OK eid <-> mid = ${inputID} <-> ${messageID}`)
|
||||||
|
return false // message was found so don't retrigger
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugRetrigger(`[retrigger] WAIT mid = ${messageID}`)
|
debugRetrigger(`[retrigger] WAIT id = ${inputID}`)
|
||||||
emitter.once(messageID, () => {
|
emitter.once(inputID, () => {
|
||||||
debugRetrigger(`[retrigger] TRIGGER mid = ${messageID}`)
|
debugRetrigger(`[retrigger] TRIGGER id = ${inputID}`)
|
||||||
fn(...rest)
|
fn(...rest)
|
||||||
})
|
})
|
||||||
// if the event never arrives, don't trigger the callback, just clean up
|
// if the event never arrives, don't trigger the callback, just clean up
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (emitter.listeners(messageID).length) {
|
if (emitter.listeners(inputID).length) {
|
||||||
debugRetrigger(`[retrigger] EXPIRE mid = ${messageID}`)
|
debugRetrigger(`[retrigger] EXPIRE id = ${inputID}`)
|
||||||
}
|
}
|
||||||
emitter.removeAllListeners(messageID)
|
emitter.removeAllListeners(inputID)
|
||||||
}, 60 * 1000) // 1 minute
|
}, 60 * 1000) // 1 minute
|
||||||
return true // event was not found, then retrigger
|
return true // event was not found, then retrigger
|
||||||
}
|
}
|
||||||
|
|
@ -58,11 +66,11 @@ function eventNotFoundThenRetrigger(messageID, fn, ...rest) {
|
||||||
*/
|
*/
|
||||||
async function pauseChanges(messageID, promise) {
|
async function pauseChanges(messageID, promise) {
|
||||||
try {
|
try {
|
||||||
debugRetrigger(`[retrigger] PAUSE mid = ${messageID}`)
|
debugRetrigger(`[retrigger] PAUSE id = ${messageID}`)
|
||||||
paused.add(messageID)
|
paused.add(messageID)
|
||||||
return await promise
|
return await promise
|
||||||
} finally {
|
} finally {
|
||||||
debugRetrigger(`[retrigger] RESUME mid = ${messageID}`)
|
debugRetrigger(`[retrigger] RESUME id = ${messageID}`)
|
||||||
paused.delete(messageID)
|
paused.delete(messageID)
|
||||||
messageFinishedBridging(messageID)
|
messageFinishedBridging(messageID)
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +82,7 @@ async function pauseChanges(messageID, promise) {
|
||||||
*/
|
*/
|
||||||
function messageFinishedBridging(messageID) {
|
function messageFinishedBridging(messageID) {
|
||||||
if (emitter.listeners(messageID).length) {
|
if (emitter.listeners(messageID).length) {
|
||||||
debugRetrigger(`[retrigger] EMIT mid = ${messageID}`)
|
debugRetrigger(`[retrigger] EMIT id = ${messageID}`)
|
||||||
}
|
}
|
||||||
emitter.emit(messageID)
|
emitter.emit(messageID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,21 @@ test("message2event embeds: youtube video", async t => {
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("message2event embeds: embed not bridged if its link was spoilered", async t => {
|
||||||
|
const events = await messageToEvent({
|
||||||
|
...data.message_with_embeds.youtube_video,
|
||||||
|
content: "||https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E\n\n\nJutomi I'm gonna make these sounds in your walls tonight||"
|
||||||
|
}, data.guild.general)
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "[spoiler]",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: `<span data-mx-spoiler=""><a href="https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E">https://youtu.be/kDMHHw8JqLE?si=NaqNjVTtXugHeG_E</a><br><br><br>Jutomi I'm gonna make these sounds in your walls tonight</span>`,
|
||||||
|
"m.mentions": {}
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
test("message2event embeds: tenor gif should show a video link without a provider", async t => {
|
test("message2event embeds: tenor gif should show a video link without a provider", async t => {
|
||||||
const events = await messageToEvent(data.message_with_embeds.tenor_gif, data.guild.general, {}, {})
|
const events = await messageToEvent(data.message_with_embeds.tenor_gif, data.guild.general, {}, {})
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
||||||
* @param {DiscordTypes.APIMessage} message
|
* @param {DiscordTypes.APIMessage} message
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
* @param {boolean} useHTML
|
* @param {boolean} useHTML
|
||||||
|
* @param {string[]} spoilers
|
||||||
*/
|
*/
|
||||||
function getDiscordParseCallbacks(message, guild, useHTML) {
|
function getDiscordParseCallbacks(message, guild, useHTML, spoilers = []) {
|
||||||
return {
|
return {
|
||||||
/** @param {{id: string, type: "discordUser"}} node */
|
/** @param {{id: string, type: "discordUser"}} node */
|
||||||
user: node => {
|
user: node => {
|
||||||
|
|
@ -90,6 +91,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
|
||||||
here: () => {
|
here: () => {
|
||||||
if (message.mention_everyone) return "@room"
|
if (message.mention_everyone) return "@room"
|
||||||
return "@here"
|
return "@here"
|
||||||
|
},
|
||||||
|
spoiler: node => {
|
||||||
|
spoilers.push(node.raw)
|
||||||
|
return useHTML
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,8 +286,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// Mentions scenarios 1 and 2, part A. i.e. translate relevant message.mentions to m.mentions
|
// Mentions scenarios 1 and 2, part A. i.e. translate relevant message.mentions to m.mentions
|
||||||
// (Still need to do scenarios 1 and 2 part B, and scenario 3.)
|
// (Still need to do scenarios 1 and 2 part B, and scenario 3.)
|
||||||
if (message.type === DiscordTypes.MessageType.Reply && message.message_reference?.message_id) {
|
if (message.type === DiscordTypes.MessageType.Reply && message.message_reference?.message_id) {
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index").select("event_id", "room_id", "reference_channel_id", "source").and("WHERE message_id = ? ORDER BY part ASC").get(message.message_reference.message_id)
|
const row = await getHistoricalEventRow(message.message_reference?.message_id)
|
||||||
if (row) {
|
if (row && "event_id" in row) {
|
||||||
repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
|
repliedToEventRow = Object.assign(row, {channel_id: row.reference_channel_id})
|
||||||
} else if (message.referenced_message) {
|
} else if (message.referenced_message) {
|
||||||
repliedToUnknownEvent = true
|
repliedToUnknownEvent = true
|
||||||
|
|
@ -295,8 +300,8 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
assert(message.embeds[0].description)
|
assert(message.embeds[0].description)
|
||||||
const match = message.embeds[0].description.match(/\/channels\/[0-9]*\/[0-9]*\/([0-9]{2,})/)
|
const match = message.embeds[0].description.match(/\/channels\/[0-9]*\/[0-9]*\/([0-9]{2,})/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index").select("event_id", "room_id", "reference_channel_id", "source").and("WHERE message_id = ? ORDER BY part ASC").get(match[1])
|
const row = await getHistoricalEventRow(match[1])
|
||||||
if (row) {
|
if (row && "event_id" in row) {
|
||||||
/*
|
/*
|
||||||
we generate a partial referenced_message based on what PK provided. we don't need everything, since this will only be used for further message-to-event converting.
|
we generate a partial referenced_message based on what PK provided. we don't need everything, since this will only be used for further message-to-event converting.
|
||||||
the following properties are necessary:
|
the following properties are necessary:
|
||||||
|
|
@ -341,6 +346,34 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} messageID
|
||||||
|
* @param {string} [timestampChannelID]
|
||||||
|
*/
|
||||||
|
async function getHistoricalEventRow(messageID, timestampChannelID) {
|
||||||
|
/** @type {{room_id: string} | {event_id: string, room_id: string, reference_channel_id: string, source: number} | null} */
|
||||||
|
let row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
|
.select("event_id", "room_id", "reference_channel_id", "source").where({message_id: messageID}).and("ORDER BY part ASC").get()
|
||||||
|
if (!row && timestampChannelID) {
|
||||||
|
const ts = dUtils.snowflakeToTimestampExact(messageID)
|
||||||
|
const oldestRow = from("historical_channel_room").selectUnsafe("max(upgraded_timestamp)", "room_id")
|
||||||
|
.where({reference_channel_id: timestampChannelID}).and("and upgraded_timestamp < ?").get(ts)
|
||||||
|
if (oldestRow?.room_id) {
|
||||||
|
row = {room_id: oldestRow.room_id}
|
||||||
|
try {
|
||||||
|
const {event_id} = await di.api.getEventForTimestamp(oldestRow.room_id, ts)
|
||||||
|
row = {
|
||||||
|
event_id,
|
||||||
|
room_id: oldestRow.room_id,
|
||||||
|
reference_channel_id: oldestRow.reference_channel_id,
|
||||||
|
source: 1
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate Discord message links to Matrix event links.
|
* Translate Discord message links to Matrix event links.
|
||||||
* If OOYE has handled this message in the past, this is an instant database lookup.
|
* If OOYE has handled this message in the past, this is an instant database lookup.
|
||||||
|
|
@ -353,30 +386,11 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
assert(typeof match.index === "number")
|
assert(typeof match.index === "number")
|
||||||
const [_, channelID, messageID] = match
|
const [_, channelID, messageID] = match
|
||||||
const result = await (async () => {
|
const result = await (async () => {
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = await getHistoricalEventRow(messageID, channelID)
|
||||||
.select("event_id", "room_id").where({message_id: messageID}).get()
|
if (!row) return `${match[0]} [event is from another server]`
|
||||||
// const roomID = select("channel_room", "room_id", {channel_id: channelID}).pluck().get()
|
|
||||||
if (row) {
|
|
||||||
const via = await getViaServersMemo(row.room_id)
|
const via = await getViaServersMemo(row.room_id)
|
||||||
|
if (!("event_id" in row)) return `[unknown event in https://matrix.to/#/${row.room_id}?${via}]`
|
||||||
return `https://matrix.to/#/${row.room_id}/${row.event_id}?${via}`
|
return `https://matrix.to/#/${row.room_id}/${row.event_id}?${via}`
|
||||||
}
|
|
||||||
|
|
||||||
const ts = dUtils.snowflakeToTimestampExact(messageID)
|
|
||||||
const oldestRow = from("historical_channel_room").selectUnsafe("max(upgraded_timestamp)", "room_id")
|
|
||||||
.where({reference_channel_id: channelID}).and("and upgraded_timestamp < ?").get(ts)
|
|
||||||
if (oldestRow?.room_id) {
|
|
||||||
const via = await getViaServersMemo(oldestRow.room_id)
|
|
||||||
try {
|
|
||||||
const {event_id} = await di.api.getEventForTimestamp(oldestRow.room_id, ts)
|
|
||||||
return `https://matrix.to/#/${oldestRow.room_id}/${event_id}?${via}`
|
|
||||||
} catch (e) {
|
|
||||||
// M_NOT_FOUND: Unable to find event from <ts> in direction Direction.FORWARDS
|
|
||||||
// not supported in Conduit and descendants
|
|
||||||
return `[unknown event, timestamp resolution failed, in room: https://matrix.to/#/${oldestRow.room_id}?${via}]`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${match[0]} [event is from another server]`
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
content = content.slice(0, match.index + offset) + result + content.slice(match.index + match[0].length + offset)
|
content = content.slice(0, match.index + offset) + result + content.slice(match.index + match[0].length + offset)
|
||||||
|
|
@ -392,6 +406,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
return content.replace(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/g, url => dUtils.getPublicUrlForCdn(url))
|
return content.replace(/https:\/\/(cdn|media)\.discordapp\.(?:com|net)\/attachments\/([0-9]+)\/([0-9]+)\/([-A-Za-z0-9_.,]+)/g, url => dUtils.getPublicUrlForCdn(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spoilers = []
|
||||||
/**
|
/**
|
||||||
* Translate links and emojis and mentions and stuff. Give back the text and HTML so they can be combined into bigger events.
|
* Translate links and emojis and mentions and stuff. Give back the text and HTML so they can be combined into bigger events.
|
||||||
* @param {string} content Partial or complete Discord message content
|
* @param {string} content Partial or complete Discord message content
|
||||||
|
|
@ -431,7 +446,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
let html = await markdown.toHtmlWithPostParser(content, transformParsedVia, {
|
||||||
discordCallback: getDiscordParseCallbacks(message, guild, true),
|
discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers),
|
||||||
...customOptions
|
...customOptions
|
||||||
}, customParser, customHtmlOutput)
|
}, customParser, customHtmlOutput)
|
||||||
|
|
||||||
|
|
@ -555,17 +570,16 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// Forwarded content appears first
|
// Forwarded content appears first
|
||||||
if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_snapshots?.length) {
|
if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_snapshots?.length) {
|
||||||
// Forwarded notice
|
// Forwarded notice
|
||||||
const event = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = await getHistoricalEventRow(message.message_reference.message_id, message.message_reference.channel_id)
|
||||||
.select("event_id", "room_id").where({message_id: message.message_reference.message_id}).get()
|
|
||||||
const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get()
|
const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get()
|
||||||
const forwardedNotice = new mxUtils.MatrixStringBuilder()
|
const forwardedNotice = new mxUtils.MatrixStringBuilder()
|
||||||
if (room) {
|
if (room) {
|
||||||
const roomName = room && (room.nick || room.name)
|
const roomName = room && (room.nick || room.name)
|
||||||
if (event) {
|
if ("event_id" in row) {
|
||||||
const via = await getViaServersMemo(event.room_id)
|
const via = await getViaServersMemo(row.room_id)
|
||||||
forwardedNotice.addLine(
|
forwardedNotice.addLine(
|
||||||
`[🔀 Forwarded from #${roomName}]`,
|
`[🔀 Forwarded from #${roomName}]`,
|
||||||
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}/${event.event_id}?${via}">[jump to event]</a></em>`
|
tag`🔀 <em>Forwarded from ${roomName} <a href="https://matrix.to/#/${room.room_id}/${row.event_id}?${via}">[jump to event]</a></em>`
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const via = await getViaServersMemo(room.room_id)
|
const via = await getViaServersMemo(room.room_id)
|
||||||
|
|
@ -692,6 +706,13 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
continue // If discord creates an embed preview for a discord channel link, don't copy that embed
|
continue // If discord creates an embed preview for a discord channel link, don't copy that embed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (embed.url && spoilers.some(sp => sp.match(/\bhttps?:\/\/[a-z]/))) {
|
||||||
|
// If the original message had spoilered URLs, don't generate any embeds for links.
|
||||||
|
// This logic is the same as the Discord desktop client. It doesn't match specific embeds to specific spoilered text, it's all or nothing.
|
||||||
|
// It's not easy to do much better because posting a link like youtu.be generates an embed.url with youtube.com/watch, so you can't match up the text without making at least that a special case.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
// 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()
|
const rep = new mxUtils.MatrixStringBuilder()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,10 +287,10 @@ test("message2event: message timestamp failed to fetch", async t => {
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "Me: I'll scroll up to find a certain message I'll send\n_scrolls up and clicks message links for god knows how long_\n_completely forgets what they were looking for and simply begins scrolling up to find some fun moments_\n_stumbles upon:_ "
|
body: "Me: I'll scroll up to find a certain message I'll send\n_scrolls up and clicks message links for god knows how long_\n_completely forgets what they were looking for and simply begins scrolling up to find some fun moments_\n_stumbles upon:_ "
|
||||||
+ "[unknown event, timestamp resolution failed, in room: https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org]",
|
+ "[unknown event in https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org]",
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: "Me: I'll scroll up to find a certain message I'll send<br><em>scrolls up and clicks message links for god knows how long</em><br><em>completely forgets what they were looking for and simply begins scrolling up to find some fun moments</em><br><em>stumbles upon:</em> "
|
formatted_body: "Me: I'll scroll up to find a certain message I'll send<br><em>scrolls up and clicks message links for god knows how long</em><br><em>completely forgets what they were looking for and simply begins scrolling up to find some fun moments</em><br><em>stumbles upon:</em> "
|
||||||
+ '[unknown event, timestamp resolution failed, in room: <a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org">https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org</a>]'
|
+ '[unknown event in <a href="https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org">https://matrix.to/#/!kLRqKKUQXcibIMtOpl:cadence.moe?via=cadence.moe&via=matrix.org</a>]'
|
||||||
}])
|
}])
|
||||||
t.equal(called, 2, "getEventForTimestamp and getJoinedMembers should be called once each")
|
t.equal(called, 2, "getEventForTimestamp and getJoinedMembers should be called once each")
|
||||||
})
|
})
|
||||||
|
|
@ -862,6 +862,20 @@ test("message2event: advanced written @mentions for matrix users", async t => {
|
||||||
t.equal(called, 1, "should only look up the member list once")
|
t.equal(called, 1, "should only look up the member list once")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("message2event: spoilers are removed from plaintext body", async t => {
|
||||||
|
const events = await messageToEvent({
|
||||||
|
content: "||**beatrice**||"
|
||||||
|
})
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": {},
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: "[spoiler]",
|
||||||
|
format: "org.matrix.custom.html",
|
||||||
|
formatted_body: `<span data-mx-spoiler=""><strong>beatrice</strong></span>`
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
test("message2event: very large attachment is linked instead of being uploaded", async t => {
|
test("message2event: very large attachment is linked instead of being uploaded", async t => {
|
||||||
const events = await messageToEvent({
|
const events = await messageToEvent({
|
||||||
content: "hey",
|
content: "hey",
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,27 @@ const assert = require("assert").strict
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select} = passthrough
|
const {discord, as, sync, db, select, from} = passthrough
|
||||||
/** @type {import("../../matrix/utils")} */
|
/** @type {import("../../matrix/utils")} */
|
||||||
const utils = sync.require("../../matrix/utils")
|
const utils = sync.require("../../matrix/utils")
|
||||||
/** @type {import("../converters/emoji")} */
|
/** @type {import("../converters/emoji")} */
|
||||||
const emoji = sync.require("../converters/emoji")
|
const emoji = sync.require("../converters/emoji")
|
||||||
|
/** @type {import("../../d2m/actions/retrigger")} */
|
||||||
|
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event
|
* @param {Ty.Event.Outer<Ty.Event.M_Reaction>} event
|
||||||
*/
|
*/
|
||||||
async function addReaction(event) {
|
async function addReaction(event) {
|
||||||
const channelID = select("historical_channel_room", "reference_channel_id", {room_id: event.room_id}).pluck().get()
|
// Wait until the corresponding channel and message have already been bridged
|
||||||
if (!channelID) return // We just assume the bridge has already been created
|
if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, as.emit.bind(as, "type:m.reaction", event))) return
|
||||||
const messageID = select("event_message", "message_id", {event_id: event.content["m.relates_to"].event_id}, "ORDER BY reaction_part").pluck().get()
|
|
||||||
if (!messageID) return // Nothing can be done if the parent message was never bridged.
|
// These will exist because it passed retrigger
|
||||||
|
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
|
.select("message_id", "reference_channel_id").where({event_id: event.content["m.relates_to"].event_id}).and("ORDER BY reaction_part ASC").get()
|
||||||
|
assert(row)
|
||||||
|
const messageID = row.message_id
|
||||||
|
const channelID = row.reference_channel_id
|
||||||
|
|
||||||
const key = event.content["m.relates_to"].key
|
const key = event.content["m.relates_to"].key
|
||||||
const discordPreferredEncoding = await emoji.encodeEmoji(key, event.content.shortcode)
|
const discordPreferredEncoding = await emoji.encodeEmoji(key, event.content.shortcode)
|
||||||
|
|
@ -35,6 +42,10 @@ async function addReaction(event) {
|
||||||
// happens if a matrix user tries to add on to a super reaction
|
// happens if a matrix user tries to add on to a super reaction
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (e.message?.includes("Unknown Message")) {
|
||||||
|
// happens under a race condition where a message is deleted after it passes the database check above
|
||||||
|
return
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ const DiscordTypes = require("discord-api-types/v10")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, sync, db, select, from} = passthrough
|
const {discord, as, sync, db, select, from} = passthrough
|
||||||
/** @type {import("../../matrix/utils")} */
|
/** @type {import("../../matrix/utils")} */
|
||||||
const utils = sync.require("../../matrix/utils")
|
const utils = sync.require("../../matrix/utils")
|
||||||
|
/** @type {import("../../d2m/actions/retrigger")} */
|
||||||
|
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
|
|
@ -52,13 +54,18 @@ async function removeReaction(event) {
|
||||||
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
* @param {Ty.Event.Outer_M_Room_Redaction} event
|
||||||
*/
|
*/
|
||||||
async function handle(event) {
|
async function handle(event) {
|
||||||
|
// If this is for removing a reaction, try it
|
||||||
|
await removeReaction(event)
|
||||||
|
|
||||||
|
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
||||||
|
if (retrigger.eventNotFoundThenRetrigger(event.redacts, as.emit.bind(as, "type:m.room.redaction", event))) return
|
||||||
|
|
||||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||||
await suppressEmbeds(event)
|
await suppressEmbeds(event)
|
||||||
} else {
|
} else {
|
||||||
await deleteMessage(event)
|
await deleteMessage(event)
|
||||||
}
|
}
|
||||||
await removeReaction(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.handle = handle
|
module.exports.handle = handle
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ const api = sync.require("../matrix/api")
|
||||||
const createRoom = sync.require("../d2m/actions/create-room")
|
const createRoom = sync.require("../d2m/actions/create-room")
|
||||||
/** @type {import("../matrix/room-upgrade")} */
|
/** @type {import("../matrix/room-upgrade")} */
|
||||||
const roomUpgrade = require("../matrix/room-upgrade")
|
const roomUpgrade = require("../matrix/room-upgrade")
|
||||||
|
/** @type {import("../d2m/actions/retrigger")} */
|
||||||
|
const retrigger = sync.require("../d2m/actions/retrigger")
|
||||||
const {reg} = require("../matrix/read-registration")
|
const {reg} = require("../matrix/read-registration")
|
||||||
|
|
||||||
let lastReportedEvent = 0
|
let lastReportedEvent = 0
|
||||||
|
|
@ -201,6 +203,7 @@ async event => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await matrixCommandHandler.execute(event)
|
await matrixCommandHandler.execute(event)
|
||||||
}
|
}
|
||||||
|
retrigger.messageFinishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -211,6 +214,7 @@ sync.addTemporaryListener(as, "type:m.sticker", guard("m.sticker",
|
||||||
async event => {
|
async event => {
|
||||||
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
if (utils.eventSenderIsFromDiscord(event.sender)) return
|
||||||
const messageResponses = await sendEvent.sendEvent(event)
|
const messageResponses = await sendEvent.sendEvent(event)
|
||||||
|
retrigger.messageFinishedBridging(event.event_id)
|
||||||
await api.ackEvent(event)
|
await api.ackEvent(event)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue