Compare commits
No commits in common. "e72836c4794a62d1721cb8f013b5ccee1746e13b" and "02d62c091442aa5eae39870922ec0753448a4866" have entirely different histories.
e72836c479
...
02d62c0914
14 changed files with 19 additions and 644 deletions
|
|
@ -21,7 +21,7 @@ const mreq = sync.require("../../matrix/mreq")
|
||||||
async function editMessage(message, guild, row) {
|
async function editMessage(message, guild, row) {
|
||||||
const historicalRoomOfMessage = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: message.id}).select("room_id").get()
|
const historicalRoomOfMessage = from("message_room").join("historical_channel_room", "historical_room_index").where({message_id: message.id}).select("room_id").get()
|
||||||
const currentRoom = from("channel_room").join("historical_channel_room", "room_id").where({channel_id: message.channel_id}).select("room_id", "historical_room_index").get()
|
const currentRoom = from("channel_room").join("historical_channel_room", "room_id").where({channel_id: message.channel_id}).select("room_id", "historical_room_index").get()
|
||||||
if (!currentRoom) return
|
assert(currentRoom)
|
||||||
|
|
||||||
if (historicalRoomOfMessage && historicalRoomOfMessage.room_id !== currentRoom.room_id) return // tombstoned rooms should not have new events (including edits) sent to them
|
if (historicalRoomOfMessage && historicalRoomOfMessage.room_id !== currentRoom.room_id) return // tombstoned rooms should not have new events (including edits) sent to them
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,6 @@ const DiscordTypes = require("discord-api-types/v10")
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {discord, select, db} = passthrough
|
const {discord, select, db} = passthrough
|
||||||
|
|
||||||
const DEBUG_SPEEDBUMP = false
|
|
||||||
|
|
||||||
function debugSpeedbump(message) {
|
|
||||||
if (DEBUG_SPEEDBUMP) {
|
|
||||||
console.log(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const SPEEDBUMP_SPEED = 4000 // 4 seconds delay
|
const SPEEDBUMP_SPEED = 4000 // 4 seconds delay
|
||||||
const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
|
const SPEEDBUMP_UPDATE_FREQUENCY = 2 * 60 * 60 // 2 hours
|
||||||
|
|
||||||
|
|
@ -35,8 +27,8 @@ async function updateCache(channelID, lastChecked) {
|
||||||
db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID)
|
db.prepare("UPDATE channel_room SET speedbump_id = ?, speedbump_webhook_id = ?, speedbump_checked = ? WHERE channel_id = ?").run(foundApplication, foundWebhook, now, channelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {Map<string, number>} messageID -> number of gateway events currently bumping */
|
/** @type {Set<string>} set of messageID */
|
||||||
const bumping = new Map()
|
const bumping = new Set()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
* Slow down a message. After it passes the speedbump, return whether it's okay or if it's been deleted.
|
||||||
|
|
@ -44,26 +36,9 @@ const bumping = new Map()
|
||||||
* @returns whether it was deleted
|
* @returns whether it was deleted
|
||||||
*/
|
*/
|
||||||
async function doSpeedbump(messageID) {
|
async function doSpeedbump(messageID) {
|
||||||
let value = (bumping.get(messageID) ?? 0) + 1
|
bumping.add(messageID)
|
||||||
bumping.set(messageID, value)
|
|
||||||
debugSpeedbump(`[speedbump] WAIT ${messageID}++ = ${value}`)
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED))
|
await new Promise(resolve => setTimeout(resolve, SPEEDBUMP_SPEED))
|
||||||
|
return !bumping.delete(messageID)
|
||||||
if (!bumping.has(messageID)) {
|
|
||||||
debugSpeedbump(`[speedbump] DELETED ${messageID}`)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
value = bumping.get(messageID) - 1
|
|
||||||
if (value === 0) {
|
|
||||||
debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`)
|
|
||||||
bumping.delete(messageID)
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
debugSpeedbump(`[speedbump] MULTI ${messageID}-- = ${value}`)
|
|
||||||
bumping.set(messageID, value)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -227,8 +227,8 @@ async function editToChanges(message, guild, api) {
|
||||||
*/
|
*/
|
||||||
function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) {
|
function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) {
|
||||||
const content = {
|
const content = {
|
||||||
"m.mentions": {},
|
|
||||||
...newFallbackContent,
|
...newFallbackContent,
|
||||||
|
"m.mentions": {},
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
...newInnerContent
|
...newInnerContent
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -107,10 +107,9 @@ const embedTitleParser = markdown.markdownEngine.parserFor({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
||||||
* @param {Omit<DiscordTypes.APIAttachment, "id" | "proxy_url">} attachment
|
* @param {DiscordTypes.APIAttachment} attachment
|
||||||
* @param {boolean} [alwaysLink]
|
|
||||||
*/
|
*/
|
||||||
async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
async function attachmentToEvent(mentions, attachment) {
|
||||||
const external_url = dUtils.getPublicUrlForCdn(attachment.url)
|
const external_url = dUtils.getPublicUrlForCdn(attachment.url)
|
||||||
const emoji =
|
const emoji =
|
||||||
attachment.content_type?.startsWith("image/jp") ? "📸"
|
attachment.content_type?.startsWith("image/jp") ? "📸"
|
||||||
|
|
@ -131,7 +130,7 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
|
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
|
||||||
else if (alwaysLink || attachment.size > reg.ooye.max_file_size) {
|
else if (attachment.size > reg.ooye.max_file_size) {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -229,7 +228,6 @@ async function pollToEvent(poll) {
|
||||||
return matrixAnswer;
|
return matrixAnswer;
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
/** @type {"org.matrix.msc3381.poll.start"} */
|
|
||||||
$type: "org.matrix.msc3381.poll.start",
|
$type: "org.matrix.msc3381.poll.start",
|
||||||
"org.matrix.msc3381.poll.start": {
|
"org.matrix.msc3381.poll.start": {
|
||||||
question: {
|
question: {
|
||||||
|
|
@ -540,7 +538,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// 1. The replied-to event is in a different room to where the reply will be sent (i.e. a room upgrade occurred between)
|
// 1. The replied-to event is in a different room to where the reply will be sent (i.e. a room upgrade occurred between)
|
||||||
// 2. The replied-to message has no corresponding Matrix event (repliedToUnknownEvent is true)
|
// 2. The replied-to message has no corresponding Matrix event (repliedToUnknownEvent is true)
|
||||||
// This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run
|
// This branch is optional - do NOT change anything apart from the reply fallback, since it may not be run
|
||||||
if ((repliedToEventRow || repliedToUnknownEvent) && options.includeReplyFallback !== false && events.length === 0) {
|
if ((repliedToEventRow || repliedToUnknownEvent) && options.includeReplyFallback !== false) {
|
||||||
const latestRoomID = repliedToEventRow ? select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() : null
|
const latestRoomID = repliedToEventRow ? select("channel_room", "room_id", {channel_id: repliedToEventRow.channel_id}).pluck().get() : null
|
||||||
if (latestRoomID !== repliedToEventRow?.room_id) repliedToEventInDifferentRoom = true
|
if (latestRoomID !== repliedToEventRow?.room_id) repliedToEventInDifferentRoom = true
|
||||||
|
|
||||||
|
|
@ -743,7 +741,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
// Then attachments
|
// Then attachments
|
||||||
if (message.attachments) {
|
if (message.attachments) {
|
||||||
const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment)))
|
const attachmentEvents = await Promise.all(message.attachments.map(attachmentToEvent.bind(null, mentions)))
|
||||||
|
|
||||||
// Try to merge attachment events with the previous event
|
// 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.
|
// 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.
|
||||||
|
|
@ -758,101 +756,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then components
|
|
||||||
if (message.components?.length) {
|
|
||||||
const stack = [new mxUtils.MatrixStringBuilder()]
|
|
||||||
/** @param {DiscordTypes.APIMessageComponent} component */
|
|
||||||
async function processComponent(component) {
|
|
||||||
// Standalone components
|
|
||||||
if (component.type === DiscordTypes.ComponentType.TextDisplay) {
|
|
||||||
const {body, html} = await transformContent(component.content)
|
|
||||||
stack[0].addParagraph(body, html)
|
|
||||||
}
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.Separator) {
|
|
||||||
stack[0].addParagraph("----", "<hr>")
|
|
||||||
}
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.File) {
|
|
||||||
const ev = await attachmentToEvent({}, {...component.file, filename: component.name, size: component.size}, true)
|
|
||||||
stack[0].addLine(ev.body, ev.formatted_body)
|
|
||||||
}
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.MediaGallery) {
|
|
||||||
const description = component.items.length === 1 ? component.items[0].description || "Image:" : "Image gallery:"
|
|
||||||
const images = component.items.map(item => {
|
|
||||||
const publicURL = dUtils.getPublicUrlForCdn(item.media.url)
|
|
||||||
return {
|
|
||||||
url: publicURL,
|
|
||||||
estimatedName: item.media.url.match(/\/([^/?]+)(\?|$)/)?.[1] || publicURL
|
|
||||||
}
|
|
||||||
})
|
|
||||||
stack[0].addLine(`🖼️ ${description} ${images.map(i => i.url).join(", ")}`, tag`🖼️ ${description} $${images.map(i => tag`<a href="${i.url}">${i.estimatedName}</a>`).join(", ")}`)
|
|
||||||
}
|
|
||||||
// string select, text input, user select, role select, mentionable select, channel select
|
|
||||||
|
|
||||||
// Components that can have things nested
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.Container) {
|
|
||||||
// May contain action row, text display, section, media gallery, separator, file
|
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
|
||||||
for (const innerComponent of component.components) {
|
|
||||||
await processComponent(innerComponent)
|
|
||||||
}
|
|
||||||
let {body, formatted_body} = stack.shift().get()
|
|
||||||
body = body.split("\n").map(l => "| " + l).join("\n")
|
|
||||||
formatted_body = `<blockquote>${formatted_body}</blockquote>`
|
|
||||||
if (stack[0].body) stack[0].body += "\n\n"
|
|
||||||
stack[0].add(body, formatted_body)
|
|
||||||
}
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.Section) {
|
|
||||||
// May contain text display, possibly more in the future
|
|
||||||
// Accessory may be button or thumbnail
|
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
|
||||||
for (const innerComponent of component.components) {
|
|
||||||
await processComponent(innerComponent)
|
|
||||||
}
|
|
||||||
if (component.accessory) {
|
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
|
||||||
await processComponent(component.accessory)
|
|
||||||
const {body, formatted_body} = stack.shift().get()
|
|
||||||
stack[0].addLine(body, formatted_body)
|
|
||||||
}
|
|
||||||
const {body, formatted_body} = stack.shift().get()
|
|
||||||
stack[0].addParagraph(body, formatted_body)
|
|
||||||
}
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.ActionRow) {
|
|
||||||
const linkButtons = component.components.filter(c => c.type === DiscordTypes.ComponentType.Button && c.style === DiscordTypes.ButtonStyle.Link)
|
|
||||||
if (linkButtons.length) {
|
|
||||||
stack[0].addLine("")
|
|
||||||
for (const linkButton of linkButtons) {
|
|
||||||
await processComponent(linkButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Components that can only be inside things
|
|
||||||
else if (component.type === DiscordTypes.ComponentType.Thumbnail) {
|
|
||||||
// May only be a section accessory
|
|
||||||
stack[0].add(`🖼️ ${component.media.url}`, tag`🖼️ <a href="${component.media.url}">${component.media.url}</a>`)
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
if (component.label) {
|
|
||||||
stack[0].add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
|
||||||
} else {
|
|
||||||
stack[0].add(component.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not handling file upload or label because they are modal-only components
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const component of message.components) {
|
|
||||||
await processComponent(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {body, formatted_body} = stack[0].get()
|
|
||||||
await addTextEvent(body, formatted_body, "m.text")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then polls
|
// Then polls
|
||||||
if (message.poll) {
|
if (message.poll) {
|
||||||
const pollEvent = await pollToEvent(message.poll)
|
const pollEvent = await pollToEvent(message.poll)
|
||||||
|
|
@ -870,7 +773,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
continue // Matrix's own URL previews are fine for images.
|
continue // Matrix's own URL previews are fine for images.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embed.type === "video" && !embed.title && message.content.includes(embed.video?.url)) {
|
if (embed.type === "video" && !embed.title && !embed.description && message.content.includes(embed.video?.url)) {
|
||||||
continue // Doesn't add extra information and the direct video URL is already there.
|
continue // Doesn't add extra information and the direct video URL is already there.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1001,7 +904,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// Strip formatted_body where equivalent to body
|
// Strip formatted_body where equivalent to body
|
||||||
if (!options.alwaysReturnFormattedBody) {
|
if (!options.alwaysReturnFormattedBody) {
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (event.$type === "m.room.message" && "msgtype" in event && ["m.text", "m.notice"].includes(event.msgtype) && event.body === event.formatted_body) {
|
if (["m.text", "m.notice"].includes(event.msgtype) && event.body === event.formatted_body) {
|
||||||
delete event.format
|
delete event.format
|
||||||
delete event.formatted_body
|
delete event.formatted_body
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
const {test} = require("supertape")
|
|
||||||
const {messageToEvent} = require("./message-to-event")
|
|
||||||
const data = require("../../../test/data")
|
|
||||||
|
|
||||||
test("message2event components: pk question mark output", async t => {
|
|
||||||
const events = await messageToEvent(data.message_with_components.pk_question_mark_response, data.guild.general, {})
|
|
||||||
t.deepEqual(events, [{
|
|
||||||
$type: "m.room.message",
|
|
||||||
body:
|
|
||||||
"| ### Lillith (INX)"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| **Display name:** Lillith (she/her)"
|
|
||||||
+ "\n| **Pronouns:** She/Her"
|
|
||||||
+ "\n| **Message count:** 3091"
|
|
||||||
+ "\n| 🖼️ https://files.inx.moe/p/cdn/lillith.webp"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| ----"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| **Proxy tags:**"
|
|
||||||
+ "\n| ``l;text``"
|
|
||||||
+ "\n| ``l:text``"
|
|
||||||
+ "\n| ``l.text``"
|
|
||||||
+ "\n| ``textl.``"
|
|
||||||
+ "\n| ``textl;``"
|
|
||||||
+ "\n| ``textl:``"
|
|
||||||
+ "\n"
|
|
||||||
+ "\n-# System ID: `xffgnx` ∙ Member ID: `pphhoh`"
|
|
||||||
+ "\n-# Created: 2025-12-31 03:16:45 UTC"
|
|
||||||
+ "\n[View on dashboard https://dash.pluralkit.me/profile/m/pphhoh] "
|
|
||||||
+ "\n"
|
|
||||||
+ "\n----"
|
|
||||||
+ "\n"
|
|
||||||
+ "\n| **System:** INX (`xffgnx`)"
|
|
||||||
+ "\n| **Member:** Lillith (`pphhoh`)"
|
|
||||||
+ "\n| **Sent by:** infinidoge1337 (@unknown-user:)"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| **Account Roles (7)**"
|
|
||||||
+ "\n| §b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping"
|
|
||||||
+ "\n| 🖼️ https://files.inx.moe/p/cdn/lillith.webp"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| ----"
|
|
||||||
+ "\n| "
|
|
||||||
+ "\n| Same hat"
|
|
||||||
+ "\n| 🖼️ Image: https://bridge.example.org/download/discordcdn/934955898965729280/1466556006527012987/image.png"
|
|
||||||
+ "\n"
|
|
||||||
+ "\n-# Original Message ID: 1466556003645657118 · <t:1769724599:f>",
|
|
||||||
format: "org.matrix.custom.html",
|
|
||||||
formatted_body: "<blockquote>"
|
|
||||||
+ "<h3>Lillith (INX)</h3>"
|
|
||||||
+ "<p><strong>Display name:</strong> Lillith (she/her)"
|
|
||||||
+ "<br><strong>Pronouns:</strong> She/Her"
|
|
||||||
+ "<br><strong>Message count:</strong> 3091</p>"
|
|
||||||
+ `🖼️ <a href="https://files.inx.moe/p/cdn/lillith.webp">https://files.inx.moe/p/cdn/lillith.webp</a>`
|
|
||||||
+ "<hr>"
|
|
||||||
+ "<p><strong>Proxy tags:</strong>"
|
|
||||||
+ "<br><code>l;text</code>"
|
|
||||||
+ "<br><code>l:text</code>"
|
|
||||||
+ "<br><code>l.text</code>"
|
|
||||||
+ "<br><code>textl.</code>"
|
|
||||||
+ "<br><code>textl;</code>"
|
|
||||||
+ "<br><code>textl:</code></p></blockquote>"
|
|
||||||
+ "<p><sub>System ID: <code>xffgnx</code> ∙ Member ID: <code>pphhoh</code></sub><br>"
|
|
||||||
+ "<sub>Created: 2025-12-31 03:16:45 UTC</sub></p>"
|
|
||||||
+ `<a href="https://dash.pluralkit.me/profile/m/pphhoh">View on dashboard</a> `
|
|
||||||
+ "<hr>"
|
|
||||||
+ "<blockquote><p><strong>System:</strong> INX (<code>xffgnx</code>)"
|
|
||||||
+ "<br><strong>Member:</strong> Lillith (<code>pphhoh</code>)"
|
|
||||||
+ "<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>`
|
|
||||||
+ "<hr>"
|
|
||||||
+ "<p>Same hat</p>"
|
|
||||||
+ `🖼️ Image: <a href="https://bridge.example.org/download/discordcdn/934955898965729280/1466556006527012987/image.png">image.png</a></blockquote>`
|
|
||||||
+ "<p><sub>Original Message ID: 1466556003645657118 · <t:1769724599:f></sub></p>",
|
|
||||||
"m.mentions": {},
|
|
||||||
msgtype: "m.text",
|
|
||||||
}])
|
|
||||||
})
|
|
||||||
|
|
@ -274,7 +274,7 @@ module.exports = {
|
||||||
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
|
// Based on looking at data they've sent me over the gateway, this is the best way to check for meaningful changes.
|
||||||
// If the message content is a string then it includes all interesting fields and is meaningful.
|
// If the message content is a string then it includes all interesting fields and is meaningful.
|
||||||
// Otherwise, if there are embeds, then the system generated URL preview embeds.
|
// Otherwise, if there are embeds, then the system generated URL preview embeds.
|
||||||
if (!(typeof data.content === "string" || "embeds" in data || "components" in data)) return
|
if (!(typeof data.content === "string" || "embeds" in data)) return
|
||||||
|
|
||||||
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
|
if (dUtils.isEphemeralMessage(data)) return // Ephemeral messages are for the eyes of the receiver only!
|
||||||
|
|
||||||
|
|
@ -282,10 +282,8 @@ module.exports = {
|
||||||
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
|
const {affected, row} = await speedbump.maybeDoSpeedbump(data.channel_id, data.id)
|
||||||
if (affected) return
|
if (affected) return
|
||||||
|
|
||||||
if (!row) {
|
|
||||||
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
// Check that the sending-to room exists, and deal with Eventual Consistency(TM)
|
||||||
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return
|
if (retrigger.eventNotFoundThenRetrigger(data.id, module.exports.MESSAGE_UPDATE, client, data)) return
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
/** @type {DiscordTypes.GatewayMessageCreateDispatchData} */
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const assert = require("assert").strict
|
|
||||||
const Ty = require("../../types")
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const {discord, sync, select, from} = require("../../passthrough")
|
|
||||||
const {id: botID} = require("../../../addbot")
|
|
||||||
const {InteractionMethods} = require("snowtransfer")
|
|
||||||
|
|
||||||
/** @type {import("../../matrix/api")} */
|
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
/** @type {import("../../matrix/utils")} */
|
|
||||||
const utils = sync.require("../../matrix/utils")
|
|
||||||
/** @type {import("../../web/routes/guild")} */
|
|
||||||
const webGuild = sync.require("../../web/routes/guild")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction
|
|
||||||
* @param {{api: typeof api}} di
|
|
||||||
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
|
||||||
*/
|
|
||||||
async function* _interactAutocomplete({data, channel}, {api}) {
|
|
||||||
function exit() {
|
|
||||||
return {createInteractionResponse: {
|
|
||||||
/** @type {DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult} */
|
|
||||||
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: []
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check it was used in a bridged channel
|
|
||||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
|
||||||
if (!roomID) return yield exit()
|
|
||||||
|
|
||||||
// Check we are in fact autocompleting the first option, the user
|
|
||||||
if (!data.options?.[0] || data.options[0].type !== DiscordTypes.ApplicationCommandOptionType.String || !data.options[0].focused) {
|
|
||||||
return yield exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {{displayname: string | null, mxid: string}[][]} */
|
|
||||||
const providedMatches = []
|
|
||||||
|
|
||||||
const input = data.options[0].value
|
|
||||||
if (input === "") {
|
|
||||||
const events = await api.getEvents(roomID, "b", {limit: 40})
|
|
||||||
const recents = new Set(events.chunk.map(e => e.sender))
|
|
||||||
const matches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL LIMIT 25").all()
|
|
||||||
matches.sort((a, b) => +recents.has(b.mxid) - +recents.has(a.mxid))
|
|
||||||
providedMatches.push(matches)
|
|
||||||
} else if (input.startsWith("@")) { // only autocomplete mxids
|
|
||||||
const query = input.replaceAll(/[%_$]/g, char => `$${char}`) + "%"
|
|
||||||
const matches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND mxid LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
|
||||||
providedMatches.push(matches)
|
|
||||||
} else {
|
|
||||||
const query = "%" + input.replaceAll(/[%_$]/g, char => `$${char}`) + "%"
|
|
||||||
const displaynameMatches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL AND displayname LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
|
||||||
// prioritise matches closer to the start
|
|
||||||
displaynameMatches.sort((a, b) => {
|
|
||||||
let ai = a.displayname.toLowerCase().indexOf(input.toLowerCase())
|
|
||||||
if (ai === -1) ai = 999
|
|
||||||
let bi = b.displayname.toLowerCase().indexOf(input.toLowerCase())
|
|
||||||
if (bi === -1) bi = 999
|
|
||||||
return ai - bi
|
|
||||||
})
|
|
||||||
providedMatches.push(displaynameMatches)
|
|
||||||
let mxidMatches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL AND mxid LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
|
||||||
mxidMatches = mxidMatches.filter(match => {
|
|
||||||
// don't include matches in domain part of mxid
|
|
||||||
if (!match.mxid.match(/^[^:]*/)?.includes(query)) return false
|
|
||||||
if (displaynameMatches.some(m => m.mxid === match.mxid)) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
providedMatches.push(mxidMatches)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge together
|
|
||||||
let matches = providedMatches.flat()
|
|
||||||
|
|
||||||
// don't include bot
|
|
||||||
matches = matches.filter(m => m.mxid !== utils.bot)
|
|
||||||
|
|
||||||
// remove duplicates and count up to 25
|
|
||||||
const limitedMatches = []
|
|
||||||
const seen = new Set()
|
|
||||||
for (const match of matches) {
|
|
||||||
if (limitedMatches.length >= 25) break
|
|
||||||
if (seen.has(match.mxid)) continue
|
|
||||||
limitedMatches.push(match)
|
|
||||||
seen.add(match.mxid)
|
|
||||||
}
|
|
||||||
|
|
||||||
yield {createInteractionResponse: {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: limitedMatches.map(row => ({name: (row.displayname || row.mxid).slice(0, 100), value: row.mxid.slice(0, 100)}))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
|
||||||
* @param {{api: typeof api}} di
|
|
||||||
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
|
||||||
*/
|
|
||||||
async function* _interactCommand({data, channel, guild_id}, {api}) {
|
|
||||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
|
||||||
if (!roomID) {
|
|
||||||
return yield {createInteractionResponse: {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
|
||||||
content: "This channel isn't bridged to Matrix."
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(data.options?.[0]?.type === DiscordTypes.ApplicationCommandOptionType.String)
|
|
||||||
const mxid = data.options[0].value
|
|
||||||
if (!mxid.match(/^@[^:]*:./)) {
|
|
||||||
return yield {createInteractionResponse: {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
|
||||||
// embeds: [{
|
|
||||||
// description: "⚠️ To use /ping, you must select an option from autocomplete, or type a full Matrix ID.",
|
|
||||||
// footer: {
|
|
||||||
// text: "Tip: This command is not necessary. You can also ping Matrix users just by typing @their name in your message. It won't look like anything, but it does go through."
|
|
||||||
// }
|
|
||||||
// }]
|
|
||||||
content: "⚠️ To use `/ping`, you must select an option from autocomplete, or type a full Matrix ID.\n> Tip: This command is not necessary. You can also ping Matrix users just by typing @their name in your message. It won't look like anything, but it does go through."
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield {createInteractionResponse: {
|
|
||||||
type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource
|
|
||||||
}}
|
|
||||||
|
|
||||||
try {
|
|
||||||
/** @type {Ty.Event.M_Room_Member} */
|
|
||||||
var member = await api.getStateEvent(roomID, "m.room.member", mxid)
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
if (!member || member.membership !== "join") {
|
|
||||||
const inChannels = discord.guildChannelMap.get(guild_id)
|
|
||||||
.map(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}).get())
|
|
||||||
if (inChannels.length) {
|
|
||||||
return yield {editOriginalInteractionResponse: {
|
|
||||||
content: `That person isn't in this channel. They have only joined the following channels:\n${inChannels.map(c => `<#${c.id}>`).join(" • ")}\nYou can ask them to join this channel with \`/invite\`.`,
|
|
||||||
}}
|
|
||||||
} else {
|
|
||||||
return yield {editOriginalInteractionResponse: {
|
|
||||||
content: "That person isn't in this channel. You can invite them with `/invite`."
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield {editOriginalInteractionResponse: {
|
|
||||||
content: "@" + (member.displayname || mxid)
|
|
||||||
}}
|
|
||||||
|
|
||||||
yield {createFollowupMessage: {
|
|
||||||
flags: DiscordTypes.MessageFlags.Ephemeral | DiscordTypes.MessageFlags.IsComponentsV2,
|
|
||||||
components: [{
|
|
||||||
type: DiscordTypes.ComponentType.Container,
|
|
||||||
components: [{
|
|
||||||
type: DiscordTypes.ComponentType.TextDisplay,
|
|
||||||
content: "Tip: This command is not necessary. You can also ping Matrix users just by typing @their name in your message. It won't look like anything, but it does go through."
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* c8 ignore start */
|
|
||||||
|
|
||||||
/** @param {(DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}) | DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction */
|
|
||||||
async function interact(interaction) {
|
|
||||||
if (interaction.type === DiscordTypes.InteractionType.ApplicationCommandAutocomplete) {
|
|
||||||
for await (const response of _interactAutocomplete(interaction, {api})) {
|
|
||||||
if (response.createInteractionResponse) {
|
|
||||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for await (const response of _interactCommand(interaction, {api})) {
|
|
||||||
if (response.createInteractionResponse) {
|
|
||||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
|
||||||
} else if (response.editOriginalInteractionResponse) {
|
|
||||||
await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse)
|
|
||||||
} else if (response.createFollowupMessage) {
|
|
||||||
await discord.snow.interaction.createFollowupMessage(botID, interaction.token, response.createFollowupMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.interact = interact
|
|
||||||
|
|
@ -10,7 +10,6 @@ const permissions = sync.require("./interactions/permissions.js")
|
||||||
const reactions = sync.require("./interactions/reactions.js")
|
const reactions = sync.require("./interactions/reactions.js")
|
||||||
const privacy = sync.require("./interactions/privacy.js")
|
const privacy = sync.require("./interactions/privacy.js")
|
||||||
const poll = sync.require("./interactions/poll.js")
|
const poll = sync.require("./interactions/poll.js")
|
||||||
const ping = sync.require("./interactions/ping.js")
|
|
||||||
|
|
||||||
// User must have EVERY permission in default_member_permissions to be able to use the command
|
// User must have EVERY permission in default_member_permissions to be able to use the command
|
||||||
|
|
||||||
|
|
@ -39,20 +38,6 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
||||||
description: "The Matrix user to invite, e.g. @username:example.org",
|
description: "The Matrix user to invite, e.g. @username:example.org",
|
||||||
name: "user"
|
name: "user"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
}, {
|
|
||||||
name: "ping",
|
|
||||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
|
||||||
type: DiscordTypes.ApplicationCommandType.ChatInput,
|
|
||||||
description: "Ping a Matrix user.",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: DiscordTypes.ApplicationCommandOptionType.String,
|
|
||||||
description: "Display name or ID of the Matrix user",
|
|
||||||
name: "user",
|
|
||||||
autocomplete: true,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
name: "privacy",
|
name: "privacy",
|
||||||
|
|
@ -109,8 +94,6 @@ async function dispatchInteraction(interaction) {
|
||||||
await permissions.interactEdit(interaction)
|
await permissions.interactEdit(interaction)
|
||||||
} else if (interactionId === "Reactions") {
|
} else if (interactionId === "Reactions") {
|
||||||
await reactions.interact(interaction)
|
await reactions.interact(interaction)
|
||||||
} else if (interactionId === "ping") {
|
|
||||||
await ping.interact(interaction)
|
|
||||||
} else if (interactionId === "privacy") {
|
} else if (interactionId === "privacy") {
|
||||||
await privacy.interact(interaction)
|
await privacy.interact(interaction)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -128,19 +128,6 @@ async function getEventForTimestamp(roomID, ts) {
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} roomID
|
|
||||||
* @param {"b" | "f"} dir
|
|
||||||
* @param {{from?: string, limit?: any}} [pagination]
|
|
||||||
* @param {any} [filter]
|
|
||||||
*/
|
|
||||||
async function getEvents(roomID, dir, pagination = {}, filter) {
|
|
||||||
filter = filter && JSON.stringify(filter)
|
|
||||||
/** @type {Ty.Pagination<Ty.Event.Outer<any>>} */
|
|
||||||
const root = await mreq.mreq("GET", path(`/client/v3/rooms/${roomID}/messages`, null, {...pagination, dir, filter}))
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
* @returns {Promise<Ty.Event.StateOuter<any>[]>}
|
* @returns {Promise<Ty.Event.StateOuter<any>[]>}
|
||||||
|
|
@ -596,7 +583,6 @@ module.exports.leaveRoom = leaveRoom
|
||||||
module.exports.leaveRoomWithReason = leaveRoomWithReason
|
module.exports.leaveRoomWithReason = leaveRoomWithReason
|
||||||
module.exports.getEvent = getEvent
|
module.exports.getEvent = getEvent
|
||||||
module.exports.getEventForTimestamp = getEventForTimestamp
|
module.exports.getEventForTimestamp = getEventForTimestamp
|
||||||
module.exports.getEvents = getEvents
|
|
||||||
module.exports.getAllState = getAllState
|
module.exports.getAllState = getAllState
|
||||||
module.exports.getStateEvent = getStateEvent
|
module.exports.getStateEvent = getStateEvent
|
||||||
module.exports.getStateEventOuter = getStateEventOuter
|
module.exports.getStateEventOuter = getStateEventOuter
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,7 @@ class MatrixStringBuilder {
|
||||||
if (formattedBody == undefined) formattedBody = body
|
if (formattedBody == undefined) formattedBody = body
|
||||||
if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n\n"
|
if (this.body.length && this.body.slice(-1) !== "\n") this.body += "\n\n"
|
||||||
this.body += body
|
this.body += body
|
||||||
const match = formattedBody.match(/^<([a-zA-Z]+[a-zA-Z0-9]*)/)
|
formattedBody = `<p>${formattedBody}</p>`
|
||||||
if (!match || !BLOCK_ELEMENTS.includes(match[1].toUpperCase())) formattedBody = `<p>${formattedBody}</p>`
|
|
||||||
this.formattedBody += formattedBody
|
this.formattedBody += formattedBody
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
|
|
|
||||||
188
test/data.js
188
test/data.js
|
|
@ -4975,194 +4975,6 @@ module.exports = {
|
||||||
tts: false
|
tts: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message_with_components: {
|
|
||||||
pk_question_mark_response: {
|
|
||||||
type: 0,
|
|
||||||
content: '',
|
|
||||||
mentions: [],
|
|
||||||
mention_roles: [],
|
|
||||||
attachments: [],
|
|
||||||
embeds: [],
|
|
||||||
timestamp: '2026-01-30T01:20:07.488000+00:00',
|
|
||||||
edited_timestamp: null,
|
|
||||||
flags: 32768,
|
|
||||||
author: {
|
|
||||||
id: '772659086046658620',
|
|
||||||
username: 'cadence.worm',
|
|
||||||
avatar: '466df0c98b1af1e1388f595b4c1ad1b9',
|
|
||||||
discriminator: '0',
|
|
||||||
public_flags: 0,
|
|
||||||
flags: 0,
|
|
||||||
banner: null,
|
|
||||||
accent_color: null,
|
|
||||||
global_name: 'cadence',
|
|
||||||
avatar_decoration_data: null,
|
|
||||||
collectibles: null,
|
|
||||||
display_name_styles: null,
|
|
||||||
banner_color: null,
|
|
||||||
clan: {
|
|
||||||
identity_guild_id: '532245108070809601',
|
|
||||||
identity_enabled: true,
|
|
||||||
tag: 'doll',
|
|
||||||
badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
|
|
||||||
},
|
|
||||||
primary_guild: {
|
|
||||||
identity_guild_id: '532245108070809601',
|
|
||||||
identity_enabled: true,
|
|
||||||
tag: 'doll',
|
|
||||||
badge: 'dba08126b4e810a0e096cc7cd5bc37f0'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 17,
|
|
||||||
id: 1,
|
|
||||||
accent_color: 1042150,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
id: 2,
|
|
||||||
components: [
|
|
||||||
{ type: 10, id: 3, content: '### Lillith (INX)' },
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
id: 4,
|
|
||||||
content: '**Display name:** Lillith (she/her)\n' +
|
|
||||||
'**Pronouns:** She/Her\n' +
|
|
||||||
'**Message count:** 3091'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 11,
|
|
||||||
id: 5,
|
|
||||||
media: {
|
|
||||||
id: '1466603856149610687',
|
|
||||||
url: 'https://files.inx.moe/p/cdn/lillith.webp',
|
|
||||||
proxy_url: 'https://images-ext-1.discordapp.net/external/Kn5b32mM4o8AAQbq0k39KOzp9-fy6D1tWKvK_XI27LI/https/files.inx.moe/p/cdn/lillith.webp',
|
|
||||||
width: 256,
|
|
||||||
height: 256,
|
|
||||||
placeholder: 'KVoKJwSnt7lZl5ecj1mal5eGWjAHZXIA',
|
|
||||||
placeholder_version: 1,
|
|
||||||
content_scan_metadata: { version: 4, flags: 0 },
|
|
||||||
content_type: 'image/webp',
|
|
||||||
loading_state: 2,
|
|
||||||
flags: 0
|
|
||||||
},
|
|
||||||
description: null,
|
|
||||||
spoiler: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 14, id: 6, spacing: 1, divider: true },
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
id: 7,
|
|
||||||
content: '**Proxy tags:**\n' +
|
|
||||||
'``l;text``\n' +
|
|
||||||
'``l:text``\n' +
|
|
||||||
'``l.text``\n' +
|
|
||||||
'``textl.``\n' +
|
|
||||||
'``textl;``\n' +
|
|
||||||
'``textl:``'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
spoiler: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
id: 8,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
id: 9,
|
|
||||||
content: '-# System ID: `xffgnx` ∙ Member ID: `pphhoh`\n' +
|
|
||||||
'-# Created: 2025-12-31 03:16:45 UTC'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 2,
|
|
||||||
id: 10,
|
|
||||||
style: 5,
|
|
||||||
label: 'View on dashboard',
|
|
||||||
url: 'https://dash.pluralkit.me/profile/m/pphhoh'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 14, id: 11, spacing: 1, divider: true },
|
|
||||||
{
|
|
||||||
type: 17,
|
|
||||||
id: 12,
|
|
||||||
accent_color: null,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 9,
|
|
||||||
id: 13,
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
id: 14,
|
|
||||||
content: '**System:** INX (`xffgnx`)\n' +
|
|
||||||
'**Member:** Lillith (`pphhoh`)\n' +
|
|
||||||
'**Sent by:** infinidoge1337 (<@197126718400626689>)\n' +
|
|
||||||
'\n' +
|
|
||||||
'**Account Roles (7)**\n' +
|
|
||||||
'§b, !, ‼, Ears Port Ping, Ears Update Ping, Yttr Ping, unsup Ping'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
accessory: {
|
|
||||||
type: 11,
|
|
||||||
id: 15,
|
|
||||||
media: {
|
|
||||||
id: '1466603856149610689',
|
|
||||||
url: 'https://files.inx.moe/p/cdn/lillith.webp',
|
|
||||||
proxy_url: 'https://images-ext-1.discordapp.net/external/Kn5b32mM4o8AAQbq0k39KOzp9-fy6D1tWKvK_XI27LI/https/files.inx.moe/p/cdn/lillith.webp',
|
|
||||||
width: 256,
|
|
||||||
height: 256,
|
|
||||||
placeholder: 'KVoKJwSnt7lZl5ecj1mal5eGWjAHZXIA',
|
|
||||||
placeholder_version: 1,
|
|
||||||
content_scan_metadata: { version: 4, flags: 0 },
|
|
||||||
content_type: 'image/webp',
|
|
||||||
loading_state: 2,
|
|
||||||
flags: 0
|
|
||||||
},
|
|
||||||
description: null,
|
|
||||||
spoiler: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 14, id: 16, spacing: 2, divider: true },
|
|
||||||
{ type: 10, id: 17, content: 'Same hat' },
|
|
||||||
{
|
|
||||||
type: 12,
|
|
||||||
id: 18,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
media: {
|
|
||||||
id: '1466603856149610690',
|
|
||||||
url: 'https://cdn.discordapp.com/attachments/934955898965729280/1466556006527012987/image.png?ex=697d2c37&is=697bdab7&hm=09c5028be61ce01ebbdda5c79c42e4dc10d053ce0c4b12c9d84135a0708e9db6&',
|
|
||||||
proxy_url: 'https://media.discordapp.net/attachments/934955898965729280/1466556006527012987/image.png?ex=697d2c37&is=697bdab7&hm=09c5028be61ce01ebbdda5c79c42e4dc10d053ce0c4b12c9d84135a0708e9db6&',
|
|
||||||
width: 285,
|
|
||||||
height: 126,
|
|
||||||
placeholder: '0PcBA4BqSIl9t/dnn9f0rm0=',
|
|
||||||
placeholder_version: 1,
|
|
||||||
content_scan_metadata: { version: 4, flags: 0 },
|
|
||||||
content_type: 'image/png',
|
|
||||||
loading_state: 2,
|
|
||||||
flags: 0
|
|
||||||
},
|
|
||||||
description: null,
|
|
||||||
spoiler: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
spoiler: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 10,
|
|
||||||
id: 19,
|
|
||||||
content: '-# Original Message ID: 1466556003645657118 · <t:1769724599:f>'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
message_update: {
|
message_update: {
|
||||||
edit_by_webhook: {
|
edit_by_webhook: {
|
||||||
application_id: "684280192553844747",
|
application_id: "684280192553844747",
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,8 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/d2m/converters/emoji-to-key.test")
|
require("../src/d2m/converters/emoji-to-key.test")
|
||||||
require("../src/d2m/converters/lottie.test")
|
require("../src/d2m/converters/lottie.test")
|
||||||
require("../src/d2m/converters/message-to-event.test")
|
require("../src/d2m/converters/message-to-event.test")
|
||||||
require("../src/d2m/converters/message-to-event.test.components")
|
require("../src/d2m/converters/message-to-event.embeds.test")
|
||||||
require("../src/d2m/converters/message-to-event.test.embeds")
|
require("../src/d2m/converters/message-to-event.pk.test")
|
||||||
require("../src/d2m/converters/message-to-event.test.pk")
|
|
||||||
require("../src/d2m/converters/pins-to-list.test")
|
require("../src/d2m/converters/pins-to-list.test")
|
||||||
require("../src/d2m/converters/remove-reaction.test")
|
require("../src/d2m/converters/remove-reaction.test")
|
||||||
require("../src/d2m/converters/thread-to-announcement.test")
|
require("../src/d2m/converters/thread-to-announcement.test")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue