diff --git a/src/d2m/converters/edit-to-changes.js b/src/d2m/converters/edit-to-changes.js index ff2a96f..a395209 100644 --- a/src/d2m/converters/edit-to-changes.js +++ b/src/d2m/converters/edit-to-changes.js @@ -6,8 +6,10 @@ const passthrough = require("../../passthrough") const {sync, select, from} = passthrough /** @type {import("./message-to-event")} */ const messageToEvent = sync.require("../converters/message-to-event") +/** @type {import("../../discord/utils")} */ +const dUtils = sync.require("../../discord/utils") /** @type {import("../../matrix/utils")} */ -const utils = sync.require("../../matrix/utils") +const mxUtils = sync.require("../../matrix/utils") function eventCanBeEdited(ev) { // Discord does not allow files, images, attachments, or videos to be edited. @@ -56,7 +58,7 @@ async function editToChanges(message, guild, api) { // Should be a system generated embed. We want the embed to be sent by the same user who sent the message, so that the messages get grouped in most clients. const eventID = oldEventRows[0].event_id // a calling function should have already checked that there is at least one message to edit const event = await api.getEvent(roomID, eventID) - if (utils.eventSenderIsFromDiscord(event.sender)) { + if (mxUtils.eventSenderIsFromDiscord(event.sender)) { senderMxid = event.sender } } @@ -132,14 +134,14 @@ async function editToChanges(message, guild, api) { // If this is a generated embed update, only allow the embeds to be updated, since the system only sends data about events. Ignore changes to other things. // This also prevents Matrix events that were re-subtyped during conversion (e.g. large image -> text link) from being mistakenly included. if (isGeneratedEmbed) { - unchangedEvents.push(...eventsToRedact.filter(e => e.old.event_subtype !== "m.notice")) // Move them from eventsToRedact to unchangedEvents. - eventsToRedact = eventsToRedact.filter(e => e.old.event_subtype === "m.notice") - unchangedEvents.push(...eventsToReplace.filter(e => e.old.event_subtype !== "m.notice")) // Move them from eventsToReplace to unchangedEvents. - eventsToReplace = eventsToReplace.filter(e => e.old.event_subtype === "m.notice") + unchangedEvents = unchangedEvents.concat( + dUtils.filterTo(eventsToRedact, e => e.old.event_subtype === "m.notice" && e.old.source === 1), // Move everything except embeds from eventsToRedact to unchangedEvents. + dUtils.filterTo(eventsToReplace, e => e.old.event_subtype === "m.notice" && e.old.source === 1) // Move everything except embeds from eventsToReplace to unchangedEvents. + ) eventsToSend = eventsToSend.filter(e => e.msgtype === "m.notice") // Don't send new events that aren't the embed. // Don't post new generated embeds for messages if it's been a while since the message was sent. Detached embeds look weird. - const messageTooOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 120 * 1000 // older than 2 minutes ago + const messageTooOld = message.timestamp && new Date(message.timestamp).getTime() < Date.now() - 30 * 1000 // older than 30 seconds 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 if (messageTooOld || !embedsEnabled) { @@ -150,8 +152,7 @@ async function editToChanges(message, guild, api) { // Now, everything in eventsToSend and eventsToRedact is a real change, but everything in eventsToReplace might not have actually changed! // (Example: a MESSAGE_UPDATE for a text+image message - Discord does not allow the image to be changed, but the text might have been.) // So we'll remove entries from eventsToReplace that *definitely* cannot have changed. (This is category 4 mentioned above.) Everything remaining *may* have changed. - unchangedEvents.push(...eventsToReplace.filter(ev => !eventCanBeEdited(ev))) // Move them from eventsToRedact to unchangedEvents. - eventsToReplace = eventsToReplace.filter(eventCanBeEdited) + unchangedEvents = unchangedEvents.concat(dUtils.filterTo(eventsToReplace, ev => eventCanBeEdited(ev))) // Move them from eventsToReplace to unchangedEvents. // Now, everything in eventsToReplace has the potential to have changed, but did it actually? // (Example: if a URL preview was generated or updated, the message text won't have changed.) diff --git a/src/discord/utils.js b/src/discord/utils.js index 441a55c..a51b155 100644 --- a/src/discord/utils.js +++ b/src/discord/utils.js @@ -153,6 +153,26 @@ function howOldUnbridgedMessage(oldTimestamp, newTimestamp) { return dateDisplay } +/** + * Modifies the input, removing items that don't pass the filter. Returns the items that didn't pass. + * @param {T[]} xs + * @param {(x: T, i?: number) => any} fn + * @template T + * @returns T[] + */ +function filterTo(xs, fn) { + /** @type {T[]} */ + const filtered = [] + for (let i = xs.length-1; i >= 0; i--) { + const x = xs[i] + if (!fn(x, i)) { + filtered.unshift(x) + xs.splice(i, 1) + } + } + return filtered +} + module.exports.getPermissions = getPermissions module.exports.hasPermission = hasPermission module.exports.hasSomePermissions = hasSomePermissions @@ -163,3 +183,4 @@ module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact module.exports.getPublicUrlForCdn = getPublicUrlForCdn module.exports.howOldUnbridgedMessage = howOldUnbridgedMessage +module.exports.filterTo = filterTo diff --git a/src/discord/utils.test.js b/src/discord/utils.test.js index 3a3e177..88e51c9 100644 --- a/src/discord/utils.test.js +++ b/src/discord/utils.test.js @@ -192,3 +192,10 @@ test("how old: hours", t => { test("how old: days", t => { t.equal(utils.howOldUnbridgedMessage("2024-01-01", "2025-01-01"), "a 366-day-old unbridged message") }) + +test("filterTo: works", t => { + const fruit = ["apple", "banana", "apricot"] + const rest = utils.filterTo(fruit, f => f[0] === "b") + t.deepEqual(fruit, ["banana"]) + t.deepEqual(rest, ["apple", "apricot"]) +}) diff --git a/src/web/routes/guild.js b/src/web/routes/guild.js index 47b710b..0af37e2 100644 --- a/src/web/routes/guild.js +++ b/src/web/routes/guild.js @@ -53,26 +53,6 @@ function getAPI(event) { /** @type {LRUCache} nonce to guild id */ const validNonce = new LRUCache({max: 200}) -/** - * Modifies the input, removing items that don't pass the filter. Returns the items that didn't pass. - * @param {T[]} xs - * @param {(x: T, i?: number) => any} fn - * @template T - * @returns T[] - */ -function filterTo(xs, fn) { - /** @type {T[]} */ - const filtered = [] - for (let i = xs.length-1; i >= 0; i--) { - const x = xs[i] - if (!fn(x, i)) { - filtered.unshift(x) - xs.splice(i, 1) - } - } - return filtered -} - /** * @param {{type: number, parent_id?: string, position?: number}} channel * @param {Map} channels @@ -119,15 +99,15 @@ function getChannelRoomsLinks(guild, rooms, roles) { let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all() let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c})) - let removedUncachedChannels = filterTo(linkedChannelsWithDetails, c => c.channel) + let removedUncachedChannels = dUtils.filterTo(linkedChannelsWithDetails, c => c.channel) let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id) linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels)) let unlinkedChannelIDs = channelIDs.filter(c => !linkedChannelIDs.includes(c)) /** @type {DiscordTypes.APIGuildChannel[]} */ // @ts-ignore let unlinkedChannels = unlinkedChannelIDs.map(c => discord.channels.get(c)) - let removedWrongTypeChannels = filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type)) - let removedPrivateChannels = filterTo(unlinkedChannels, c => { + let removedWrongTypeChannels = dUtils.filterTo(unlinkedChannels, c => c && [0, 5].includes(c.type)) + let removedPrivateChannels = dUtils.filterTo(unlinkedChannels, c => { const permissions = dUtils.getPermissions(guild.id, roles, guild.roles, botID, c["permission_overwrites"]) return dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel) }) @@ -135,11 +115,11 @@ function getChannelRoomsLinks(guild, rooms, roles) { let linkedRoomIDs = linkedChannels.map(c => c.room_id) let unlinkedRooms = [...rooms] - let removedLinkedRooms = filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id)) - let removedWrongTypeRooms = filterTo(unlinkedRooms, r => !r.room_type) + let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id)) + let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type) // 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 = filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) + let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/)) return { linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms, @@ -265,4 +245,3 @@ as.router.post("/api/invite", defineEventHandler(async event => { })) module.exports._getPosition = getPosition -module.exports._filterTo = filterTo diff --git a/src/web/routes/guild.test.js b/src/web/routes/guild.test.js index e0b26df..aa17548 100644 --- a/src/web/routes/guild.test.js +++ b/src/web/routes/guild.test.js @@ -4,7 +4,7 @@ const DiscordTypes = require("discord-api-types/v10") const tryToCatch = require("try-to-catch") const {router, test} = require("../../../test/web") const {MatrixServerError} = require("../../matrix/mreq") -const {_getPosition, _filterTo} = require("./guild") +const {_getPosition} = require("./guild") let nonce @@ -394,10 +394,3 @@ test("position sorting: sorts like discord does", t => { const sortedChannelIDs = [...channels.values()].sort((a, b) => _getPosition(a, channels) - _getPosition(b, channels)).map(c => c.id) t.deepEqual(sortedChannelIDs, ["first", "thread", "second", "voice", "category", "category-first", "category-second", "category-second-thread"]) }) - -test("filterTo: works", t => { - const fruit = ["apple", "banana", "apricot"] - const rest = _filterTo(fruit, f => f[0] === "b") - t.deepEqual(fruit, ["banana"]) - t.deepEqual(rest, ["apple", "apricot"]) -})