forked from cadence/out-of-your-element
Compare commits
2 commits
d53e9efe9e
...
023e1fb55f
| Author | SHA1 | Date | |
|---|---|---|---|
| 023e1fb55f | |||
| 16b63e2f2f |
29 changed files with 588 additions and 860 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
|
# Dependency justification
|
||||||
|
|
||||||
Total transitive production dependencies: 144
|
Total transitive production dependencies: 134
|
||||||
|
|
||||||
### <font size="+2">🦕</font>
|
### <font size="+2">🦕</font>
|
||||||
|
|
||||||
* (35) better-sqlite3: SQLite is the best database, and this is the best library for it.
|
* (31) 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.
|
* (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.)
|
||||||
* (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.)
|
* (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 web UI, appservice listener, authmedia proxy, and more.
|
* (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>
|
### <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/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/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/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.
|
* (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.
|
* (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.
|
* (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) 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) discord-api-types: Bitfields needed at runtime and types needed for development.
|
||||||
* (0) domino: DOM implementation that's already pulled in by turndown.
|
* (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) entities: Looks fine. No dependencies.
|
||||||
* (0) get-relative-path: Looks fine. No dependencies.
|
* (0) get-relative-path: Looks fine. No dependencies.
|
||||||
* (1) heatsync: Module hot-reloader that I trust.
|
* (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.
|
* (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) 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) 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.
|
* (0) try-to-catch: Not strictly necessary, but it's already pulled in by supertape, so I may as well.
|
||||||
|
|
|
||||||
867
package-lock.json
generated
867
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -19,14 +19,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chriscdn/promise-semaphore": "^3.0.1",
|
"@chriscdn/promise-semaphore": "^3.0.1",
|
||||||
"@cloudrac3r/discord-markdown": "^2.7.0",
|
"@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",
|
||||||
"@cloudrac3r/mixin-deep": "^3.0.1",
|
"@cloudrac3r/mixin-deep": "^3.0.1",
|
||||||
"@cloudrac3r/pngjs": "^7.0.3",
|
"@cloudrac3r/pngjs": "^7.0.3",
|
||||||
"@cloudrac3r/pug": "^4.0.4",
|
"@cloudrac3r/pug": "^4.0.4",
|
||||||
"@cloudrac3r/stream-type": "^1.0.0",
|
|
||||||
"@cloudrac3r/turndown": "^7.1.4",
|
"@cloudrac3r/turndown": "^7.1.4",
|
||||||
"@stackoverflow/stacks": "^2.5.4",
|
"@stackoverflow/stacks": "^2.5.4",
|
||||||
"@stackoverflow/stacks-icons": "^6.0.2",
|
"@stackoverflow/stacks-icons": "^6.0.2",
|
||||||
|
|
@ -47,6 +46,7 @@
|
||||||
"prettier-bytes": "^1.0.4",
|
"prettier-bytes": "^1.0.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"snowtransfer": "^0.17.5",
|
"snowtransfer": "^0.17.5",
|
||||||
|
"stream-mime-type": "^1.0.2",
|
||||||
"try-to-catch": "^4.0.5",
|
"try-to-catch": "^4.0.5",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "^0.1.2",
|
||||||
"xxhash-wasm": "^1.0.2",
|
"xxhash-wasm": "^1.0.2",
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ async function channelToKState(channel, guild, di) {
|
||||||
join_rules = {join_rule: PRIVACY_ENUMS.ROOM_JOIN_RULES[privacyLevel]}
|
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 everyoneCanSend = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.SendMessages)
|
||||||
const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)
|
const everyoneCanMentionEveryone = dUtils.hasPermission(everyonePermissions, DiscordTypes.PermissionFlagsBits.MentionEveryone)
|
||||||
|
|
||||||
|
|
@ -442,9 +442,8 @@ function syncRoom(channelID) {
|
||||||
/**
|
/**
|
||||||
* @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
|
* @param {{id: string, topic?: string?}} channel channel-ish (just needs an id, topic is optional)
|
||||||
* @param {string} guildID
|
* @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()
|
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||||
assert.ok(roomID)
|
assert.ok(roomID)
|
||||||
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
|
const row = from("guild_space").join("guild_active", "guild_id").select("space_id", "autocreate").where({guild_id: guildID}).get()
|
||||||
|
|
@ -494,7 +493,7 @@ async function unbridgeChannel(channel, guildID, messageBeforeLeave = "This room
|
||||||
// send a notification in the room
|
// send a notification in the room
|
||||||
await api.sendEvent(roomID, "m.room.message", {
|
await api.sendEvent(roomID, "m.room.message", {
|
||||||
msgtype: "m.notice",
|
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
|
// if it is an easy mode room, clean up the room from the managed space and make it clear it's not being bridged
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ function memberToPowerLevel(user, member, guild, channel) {
|
||||||
if (!member) return 0
|
if (!member) return 0
|
||||||
|
|
||||||
const permissions = dUtils.getPermissions(guild.id, member.roles, guild.roles, user.id, channel.permission_overwrites)
|
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:
|
* PL 100 = Administrator = People who can brick the room. RATIONALE:
|
||||||
* - Administrator.
|
* - Administrator.
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
// 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
|
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) {
|
if (messageReallyOld) {
|
||||||
eventsToSend = [] // Only allow edits to change and delete, but not send new.
|
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.
|
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: {
|
newContent: {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
msgtype: "m.text",
|
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",
|
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.',
|
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": {
|
"m.mentions": {
|
||||||
|
|
@ -87,7 +87,7 @@ test("edit2changes: bot response", async t => {
|
||||||
// *** Replaced With: ***
|
// *** Replaced With: ***
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
msgtype: "m.text",
|
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",
|
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.',
|
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": {
|
"m.mentions": {
|
||||||
|
|
|
||||||
|
|
@ -146,18 +146,10 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
||||||
// Highlight the relevant part of the message
|
// Highlight the relevant part of the message
|
||||||
const start = baseOffset + best.scored.matchedInputTokens[0].index
|
const start = baseOffset + best.scored.matchedInputTokens[0].index
|
||||||
const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end
|
const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end
|
||||||
const newNodes = [{
|
const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end)
|
||||||
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)
|
|
||||||
}]
|
|
||||||
return {
|
return {
|
||||||
mxid: best.mxid,
|
mxid: best.mxid,
|
||||||
newNodes
|
newContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.APIMessage} message
|
||||||
* @param {DiscordTypes.APIGuild} guild
|
* @param {DiscordTypes.APIGuild} guild
|
||||||
|
|
@ -542,60 +519,29 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed
|
return emojiToKey.emojiToKey({id, name, animated}, message.id) // Register the custom emoji if needed
|
||||||
}))
|
}))
|
||||||
|
|
||||||
async function transformParsedVia(parsed, scanTextForMentions) {
|
async function transformParsedVia(parsed) {
|
||||||
for (let n = 0; n < parsed.length; n++) {
|
for (const node of parsed) {
|
||||||
const node = parsed[n]
|
|
||||||
if (node.type === "discordChannel" || node.type === "discordChannelLink") {
|
if (node.type === "discordChannel" || node.type === "discordChannelLink") {
|
||||||
node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get()
|
node.row = select("channel_room", ["room_id", "name", "nick"], {channel_id: node.id}).get()
|
||||||
if (node.row?.room_id) {
|
if (node.row?.room_id) {
|
||||||
node.via = await getViaServersMemo(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]) {
|
for (const maybeChildNodesArray of [node, node.content, node.items]) {
|
||||||
if (Array.isArray(maybeChildNodesArray)) {
|
if (Array.isArray(maybeChildNodesArray)) {
|
||||||
await transformParsedVia(maybeChildNodesArray, scanTextForMentions && ["blockQuote", "list", "paragraph", "em", "strong", "u", "del", "text"].includes(node.type))
|
await transformParsedVia(maybeChildNodesArray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parsed
|
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),
|
discordCallback: getDiscordParseCallbacks(message, guild, true, spoilers),
|
||||||
...customOptions
|
...customOptions
|
||||||
}, customParser, customHtmlOutput)
|
}, 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),
|
discordCallback: getDiscordParseCallbacks(message, guild, false),
|
||||||
discordOnly: true,
|
discordOnly: true,
|
||||||
escapeHTML: false,
|
escapeHTML: false,
|
||||||
|
|
@ -789,12 +735,35 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
// Then text content
|
// Then text content
|
||||||
if (message.content && !isOnlyKlipyGIF && !isThinkingInteraction) {
|
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
|
// 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}>`
|
return `<:${name}:${id}>`
|
||||||
})
|
})
|
||||||
|
|
||||||
const {body, html} = await transformContent(content, {isTheMessageContent: true})
|
const {body, html} = await transformContent(content)
|
||||||
await addTextEvent(body, html, msgtype)
|
await addTextEvent(body, html, msgtype)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -858,7 +827,15 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
// 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.
|
||||||
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
|
// Then components
|
||||||
|
|
@ -996,7 +973,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
|
// 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()
|
||||||
let isAdditionalImage = false
|
|
||||||
|
|
||||||
if (isKlipyGIF) {
|
if (isKlipyGIF) {
|
||||||
assert(embed.video?.url)
|
assert(embed.video?.url)
|
||||||
|
|
@ -1063,11 +1039,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
let chosenImage = embed.image?.url
|
let chosenImage = embed.image?.url
|
||||||
// the thumbnail seems to be used for "article" type but displayed big at the bottom by discord
|
// 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 (embed.type === "article" && embed.thumbnail?.url && !chosenImage) chosenImage = embed.thumbnail.url
|
||||||
|
if (chosenImage) rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
||||||
if (chosenImage) {
|
|
||||||
isAdditionalImage = !rep.body && !!events.length
|
|
||||||
rep.addParagraph(`📸 ${dUtils.getPublicUrlForCdn(chosenImage)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`)
|
if (embed.video?.url) rep.addParagraph(`🎞️ ${dUtils.getPublicUrlForCdn(embed.video.url)}`)
|
||||||
|
|
||||||
|
|
@ -1076,11 +1048,6 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
body = body.split("\n").map(l => "| " + l).join("\n")
|
body = body.split("\n").map(l => "| " + l).join("\n")
|
||||||
html = `<blockquote>${html}</blockquote>`
|
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
|
// 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")
|
await addTextEvent(body, html, "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 => {
|
test("message2event embeds: vx image", async t => {
|
||||||
const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general)
|
const events = await messageToEvent(data.message_with_embeds.vx_image, data.guild.general)
|
||||||
t.deepEqual(events, [{
|
t.deepEqual(events, [{
|
||||||
|
|
|
||||||
|
|
@ -789,7 +789,7 @@ test("message2event: simple written @mention for matrix user", async t => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
msgtype: "m.text",
|
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",
|
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`
|
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`
|
||||||
}])
|
}])
|
||||||
|
|
@ -838,7 +838,7 @@ test("message2event: many written @mentions for matrix users", async t => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
msgtype: "m.text",
|
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",
|
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>`
|
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>`
|
||||||
}])
|
}])
|
||||||
|
|
@ -890,7 +890,7 @@ test("message2event: written @mentions may match part of the name", async t => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
msgtype: "m.text",
|
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",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@secret:cadence.moe">@cadence</a> saw this?`
|
formatted_body: `I wonder if <a href="https://matrix.to/#/@secret:cadence.moe">@cadence</a> saw this?`
|
||||||
}])
|
}])
|
||||||
|
|
@ -941,7 +941,7 @@ test("message2event: written @mentions may match part of the mxid", async t => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
msgtype: "m.text",
|
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",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `I wonder if <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a> saw this?`
|
formatted_body: `I wonder if <a href="https://matrix.to/#/@huckleton:cadence.moe">@huck</a> saw this?`
|
||||||
}])
|
}])
|
||||||
|
|
@ -962,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 => {
|
test("message2event: entire message may match elaborate display name", async t => {
|
||||||
let called = 0
|
let called = 0
|
||||||
const events = await messageToEvent({
|
const events = await messageToEvent({
|
||||||
|
|
@ -1037,7 +1007,7 @@ test("message2event: entire message may match elaborate display name", async t =
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
msgtype: "m.text",
|
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",
|
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>`
|
formatted_body: `<a href="https://matrix.to/#/@wa:cadence.moe">@Cadence, Maid of Creation, Eye of Clarity, Empress of Hope ☆</a>`
|
||||||
}])
|
}])
|
||||||
|
|
@ -1114,7 +1084,7 @@ test("message2event: multiple attachments are combined into the same event where
|
||||||
formatted_body: "hey"
|
formatted_body: "hey"
|
||||||
+ `<br>📄 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)`
|
||||||
+ `<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>`
|
+ `<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",
|
$type: "m.room.message",
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
|
|
|
||||||
|
|
@ -52,11 +52,7 @@ class DiscordClient {
|
||||||
/** @type {Map<string, Array<string>>} */
|
/** @type {Map<string, Array<string>>} */
|
||||||
this.guildChannelMap = new Map()
|
this.guildChannelMap = new Map()
|
||||||
if (listen !== "no") {
|
if (listen !== "no") {
|
||||||
this.cloud.on("event", message => {
|
this.cloud.on("event", message => discordPackets.onPacket(this, message, listen))
|
||||||
process.nextTick(() => {
|
|
||||||
discordPackets.onPacket(this, message, listen)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addEventLogger = (eventName, logName) => {
|
const addEventLogger = (eventName, logName) => {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ const utils = {
|
||||||
client.user = message.d.user
|
client.user = message.d.user
|
||||||
client.application = message.d.application
|
client.application = message.d.application
|
||||||
console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
|
console.log(`Discord logged in as ${client.user.username}#${client.user.discriminator} (${client.user.id})`)
|
||||||
interactions.registerInteractions()
|
|
||||||
|
|
||||||
} else if (message.t === "GUILD_CREATE") {
|
} 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
|
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,6 +47,7 @@ const utils = {
|
||||||
|
|
||||||
if (listen === "full") {
|
if (listen === "full") {
|
||||||
try {
|
try {
|
||||||
|
interactions.registerInteractions()
|
||||||
await eventDispatcher.checkMissedExpressions(message.d)
|
await eventDispatcher.checkMissedExpressions(message.d)
|
||||||
await eventDispatcher.checkMissedPins(client, message.d)
|
await eventDispatcher.checkMissedPins(client, message.d)
|
||||||
await eventDispatcher.checkMissedMessages(client, message.d)
|
await eventDispatcher.checkMissedMessages(client, message.d)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ const assert = require("assert").strict
|
||||||
|
|
||||||
const {reg} = require("../matrix/read-registration")
|
const {reg} = require("../matrix/read-registration")
|
||||||
|
|
||||||
const {db, select} = require("../passthrough")
|
const {db} = require("../passthrough")
|
||||||
|
|
||||||
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
/** @type {import("xxhash-wasm").XXHashAPI} */ // @ts-ignore
|
||||||
let hasher = null
|
let hasher = null
|
||||||
|
|
@ -58,15 +58,6 @@ function getPermissions(guildID, userRoles, guildRoles, userID, channelOverwrite
|
||||||
return allowed
|
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`.
|
* 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.
|
* It is designed like this to avoid developer error with bit manipulations.
|
||||||
|
|
@ -183,7 +174,6 @@ function filterTo(xs, fn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.getPermissions = getPermissions
|
module.exports.getPermissions = getPermissions
|
||||||
module.exports.getDefaultPermissions = getDefaultPermissions
|
|
||||||
module.exports.hasPermission = hasPermission
|
module.exports.hasPermission = hasPermission
|
||||||
module.exports.hasSomePermissions = hasSomePermissions
|
module.exports.hasSomePermissions = hasSomePermissions
|
||||||
module.exports.hasAllPermissions = hasAllPermissions
|
module.exports.hasAllPermissions = hasAllPermissions
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const sharp = require("sharp")
|
||||||
const api = sync.require("../../matrix/api")
|
const api = sync.require("../../matrix/api")
|
||||||
/** @type {import("../../matrix/mreq")} */
|
/** @type {import("../../matrix/mreq")} */
|
||||||
const mreq = sync.require("../../matrix/mreq")
|
const mreq = sync.require("../../matrix/mreq")
|
||||||
const {streamType} = require("@cloudrac3r/stream-type")
|
const streamMimeType = require("stream-mime-type")
|
||||||
|
|
||||||
const WIDTH = 160
|
const WIDTH = 160
|
||||||
const HEIGHT = 160
|
const HEIGHT = 160
|
||||||
|
|
@ -26,13 +26,13 @@ async function getAndResizeSticker(mxc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamIn = Readable.fromWeb(res.body)
|
const streamIn = Readable.fromWeb(res.body)
|
||||||
const {streamThrough, type} = await streamType(streamIn)
|
const { stream, mime } = await streamMimeType.getMimeType(streamIn)
|
||||||
const animated = ["image/gif", "image/webp"].includes(type)
|
const animated = ["image/gif", "image/webp"].includes(mime)
|
||||||
|
|
||||||
const transformer = sharp({animated: animated})
|
const transformer = sharp({animated: animated})
|
||||||
.resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}})
|
.resize(WIDTH, HEIGHT, {fit: "inside", background: {r: 0, g: 0, b: 0, alpha: 0}})
|
||||||
.webp()
|
.webp()
|
||||||
streamThrough.pipe(transformer)
|
stream.pipe(transformer)
|
||||||
return Readable.toWeb(transformer)
|
return Readable.toWeb(transformer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const {pipeline} = require("stream").promises
|
||||||
const sharp = require("sharp")
|
const sharp = require("sharp")
|
||||||
const {GIFrame} = require("@cloudrac3r/giframe")
|
const {GIFrame} = require("@cloudrac3r/giframe")
|
||||||
const {PNG} = require("@cloudrac3r/pngjs")
|
const {PNG} = require("@cloudrac3r/pngjs")
|
||||||
const {streamType} = require("@cloudrac3r/stream-type")
|
const streamMimeType = require("stream-mime-type")
|
||||||
|
|
||||||
const SIZE = 48
|
const SIZE = 48
|
||||||
const RESULT_WIDTH = 400
|
const RESULT_WIDTH = 400
|
||||||
|
|
@ -54,11 +54,11 @@ async function compositeMatrixEmojis(mxcs, mxcDownloader) {
|
||||||
* @returns {Promise<Buffer | undefined>} Uncompressed PNG image
|
* @returns {Promise<Buffer | undefined>} Uncompressed PNG image
|
||||||
*/
|
*/
|
||||||
async function convertImageStream(streamIn, stopStream) {
|
async function convertImageStream(streamIn, stopStream) {
|
||||||
const {streamThrough, type} = await streamType(streamIn)
|
const {stream, mime} = await streamMimeType.getMimeType(streamIn)
|
||||||
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(type), `Mime type ${type} is impossible for emojis`)
|
assert(["image/png", "image/jpeg", "image/webp", "image/gif", "image/apng"].includes(mime), `Mime type ${mime} is impossible for emojis`)
|
||||||
|
|
||||||
try {
|
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}} */
|
/** @type {{info: sharp.OutputInfo, buffer: Buffer}} */
|
||||||
const result = await new Promise((resolve, reject) => {
|
const result = await new Promise((resolve, reject) => {
|
||||||
const transformer = sharp()
|
const transformer = sharp()
|
||||||
|
|
@ -70,15 +70,15 @@ async function convertImageStream(streamIn, stopStream) {
|
||||||
resolve({info, buffer})
|
resolve({info, buffer})
|
||||||
})
|
})
|
||||||
pipeline(
|
pipeline(
|
||||||
streamThrough,
|
stream,
|
||||||
transformer
|
transformer
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return result.buffer
|
return result.buffer
|
||||||
|
|
||||||
} else if (type === "image/gif") {
|
} else if (mime === "image/gif") {
|
||||||
const giframe = new GIFrame(0)
|
const giframe = new GIFrame(0)
|
||||||
streamThrough.on("data", chunk => {
|
stream.on("data", chunk => {
|
||||||
giframe.feed(chunk)
|
giframe.feed(chunk)
|
||||||
})
|
})
|
||||||
const frame = await giframe.getFrame()
|
const frame = await giframe.getFrame()
|
||||||
|
|
@ -91,10 +91,10 @@ async function convertImageStream(streamIn, stopStream) {
|
||||||
.toBuffer({resolveWithObject: true})
|
.toBuffer({resolveWithObject: true})
|
||||||
return buffer.data
|
return buffer.data
|
||||||
|
|
||||||
} else if (type === "image/apng") {
|
} else if (mime === "image/apng") {
|
||||||
const png = new PNG({maxFrames: 1})
|
const png = new PNG({maxFrames: 1})
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
streamThrough.pipe(png)
|
stream.pipe(png)
|
||||||
/** @type {Buffer} */ // @ts-ignore
|
/** @type {Buffer} */ // @ts-ignore
|
||||||
const frame = await new Promise(resolve => png.on("parsed", resolve))
|
const frame = await new Promise(resolve => png.on("parsed", resolve))
|
||||||
stopStream()
|
stopStream()
|
||||||
|
|
|
||||||
|
|
@ -898,7 +898,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
|
let shouldSuppress = inBody !== -1 && event.content.body[inBody-1] === "<"
|
||||||
if (!shouldSuppress && guild?.roles) {
|
if (!shouldSuppress && guild?.roles) {
|
||||||
// Suppress if regular users don't have permission
|
// 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)
|
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||||
shouldSuppress = !canEmbedLinks
|
shouldSuppress = !canEmbedLinks
|
||||||
}
|
}
|
||||||
|
|
@ -961,7 +961,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
|
|
||||||
// Suppress if regular users don't have permission
|
// Suppress if regular users don't have permission
|
||||||
if (!shouldSuppress && guild?.roles) {
|
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)
|
const canEmbedLinks = dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.EmbedLinks)
|
||||||
shouldSuppress = !canEmbedLinks
|
shouldSuppress = !canEmbedLinks
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -413,7 +413,6 @@ async event => {
|
||||||
console.error(e)
|
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()})`)
|
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.`)
|
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)
|
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)
|
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)
|
||||||
|
|
@ -484,20 +483,6 @@ async event => {
|
||||||
await roomUpgrade.onTombstone(event, api)
|
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.stringifyErrorStack = stringifyErrorStack
|
||||||
module.exports.sendError = sendError
|
module.exports.sendError = sendError
|
||||||
module.exports.printError = printError
|
module.exports.printError = printError
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@ function getStateEventOuter(roomID, type, key) {
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
* @param {{unsigned?: {invite_room_state?: Ty.Event.InviteStrippedState[]}}} [event]
|
* @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) {
|
async function getInviteState(roomID, event) {
|
||||||
function getFromInviteRoomState(strippedState, nskey, key) {
|
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"),
|
name: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.name", "name"),
|
||||||
topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"),
|
topic: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.topic", "topic"),
|
||||||
avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"),
|
avatar: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.avatar", "url"),
|
||||||
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type"),
|
type: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.create", "type")
|
||||||
encryption: getFromInviteRoomState(event.unsigned.invite_room_state, "m.room.encryption", "algorithm")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,8 +227,7 @@ async function getInviteState(roomID, event) {
|
||||||
name: getFromInviteRoomState(strippedState, "m.room.name", "name"),
|
name: getFromInviteRoomState(strippedState, "m.room.name", "name"),
|
||||||
topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"),
|
topic: getFromInviteRoomState(strippedState, "m.room.topic", "topic"),
|
||||||
avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"),
|
avatar: getFromInviteRoomState(strippedState, "m.room.avatar", "url"),
|
||||||
type: getFromInviteRoomState(strippedState, "m.room.create", "type"),
|
type: getFromInviteRoomState(strippedState, "m.room.create", "type")
|
||||||
encryption: getFromInviteRoomState(strippedState, "m.room.encryption", "algorithm")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
@ -242,8 +240,7 @@ async function getInviteState(roomID, event) {
|
||||||
name: room.name ?? null,
|
name: room.name ?? null,
|
||||||
topic: room.topic ?? null,
|
topic: room.topic ?? null,
|
||||||
avatar: room.avatar_url ?? null,
|
avatar: room.avatar_url ?? null,
|
||||||
type: room.room_type ?? null,
|
type: room.room_type ?? null
|
||||||
encryption: (room.encryption || room["im.nheko.summary.encryption"]) ?? null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ async function _actuallyUploadDiscordFileToMxc(url) {
|
||||||
writeRegistration(reg)
|
writeRegistration(reg)
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
e.uploadURL = url
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,8 +105,7 @@ const commands = [{
|
||||||
// Guard
|
// Guard
|
||||||
/** @type {string} */ // @ts-ignore
|
/** @type {string} */ // @ts-ignore
|
||||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||||
const channel = discord.channels.get(channelID)
|
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||||
const guildID = channel?.["guild_id"]
|
|
||||||
let matrixOnlyReason = null
|
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."
|
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
|
// Check if we can/should upload to Discord, for various causes
|
||||||
|
|
@ -116,7 +115,7 @@ const commands = [{
|
||||||
const guild = discord.guilds.get(guildID)
|
const guild = discord.guilds.get(guildID)
|
||||||
assert(guild)
|
assert(guild)
|
||||||
const slots = getSlotCount(guild.premium_tier)
|
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) {
|
if (guild.emojis.length >= slots) {
|
||||||
matrixOnlyReason = "CAPACITY"
|
matrixOnlyReason = "CAPACITY"
|
||||||
} else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
|
} else if (!(permissions & 0x40000000n)) { // MANAGE_GUILD_EXPRESSIONS (apparently CREATE_GUILD_EXPRESSIONS isn't good enough...)
|
||||||
|
|
@ -241,8 +240,7 @@ const commands = [{
|
||||||
// Guard
|
// Guard
|
||||||
/** @type {string} */ // @ts-ignore
|
/** @type {string} */ // @ts-ignore
|
||||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||||
const channel = discord.channels.get(channelID)
|
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||||
const guildID = channel?.["guild_id"]
|
|
||||||
if (!guildID) {
|
if (!guildID) {
|
||||||
return api.sendEvent(event.room_id, "m.room.message", {
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
@ -253,7 +251,7 @@ const commands = [{
|
||||||
|
|
||||||
const guild = discord.guilds.get(guildID)
|
const guild = discord.guilds.get(guildID)
|
||||||
assert(guild)
|
assert(guild)
|
||||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||||
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
|
if (!(permissions & 0x800000000n)) { // CREATE_PUBLIC_THREADS
|
||||||
return api.sendEvent(event.room_id, "m.room.message", {
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
@ -272,8 +270,7 @@ const commands = [{
|
||||||
// Guard
|
// Guard
|
||||||
/** @type {string} */ // @ts-ignore
|
/** @type {string} */ // @ts-ignore
|
||||||
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
const channelID = select("channel_room", "channel_id", {room_id: event.room_id}).pluck().get()
|
||||||
const channel = discord.channels.get(channelID)
|
const guildID = discord.channels.get(channelID)?.["guild_id"]
|
||||||
const guildID = channel?.["guild_id"]
|
|
||||||
if (!guildID) {
|
if (!guildID) {
|
||||||
return api.sendEvent(event.room_id, "m.room.message", {
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
@ -284,7 +281,7 @@ const commands = [{
|
||||||
|
|
||||||
const guild = discord.guilds.get(guildID)
|
const guild = discord.guilds.get(guildID)
|
||||||
assert(guild)
|
assert(guild)
|
||||||
const permissions = dUtils.getDefaultPermissions(guild, channel["permission_overwrites"])
|
const permissions = dUtils.getPermissions(guild.id, [], guild.roles)
|
||||||
if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
|
if (!dUtils.hasPermission(permissions, DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
|
||||||
return api.sendEvent(event.room_id, "m.room.message", {
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
@ -293,19 +290,7 @@ const commands = [{
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const invite = await discord.snow.channel.createChannelInvite(channelID)
|
||||||
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 validHours = Math.ceil(invite.max_age / (60 * 60))
|
||||||
const validUses =
|
const validUses =
|
||||||
( invite.max_uses === 0 ? "unlimited uses"
|
( invite.max_uses === 0 ? "unlimited uses"
|
||||||
|
|
|
||||||
|
|
@ -54,17 +54,17 @@ async function onBotMembership(event, api, createRoom) {
|
||||||
assert.equal(event.type, "m.room.member")
|
assert.equal(event.type, "m.room.member")
|
||||||
assert.equal(event.state_key, utils.bot)
|
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 () => {
|
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 invited, join
|
||||||
if (event.content.membership === "invite") {
|
if (event.content.membership === "invite") {
|
||||||
await api.joinRoom(newRoomID)
|
await api.joinRoom(newRoomID)
|
||||||
|
|
|
||||||
9
src/types.d.ts
vendored
9
src/types.d.ts
vendored
|
|
@ -157,7 +157,7 @@ export namespace Event {
|
||||||
type: string
|
type: string
|
||||||
state_key: string
|
state_key: string
|
||||||
sender: 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 = {
|
export type M_Room_Create = {
|
||||||
|
|
@ -390,12 +390,6 @@ export namespace Event {
|
||||||
body: string
|
body: string
|
||||||
replacement_room: string
|
replacement_room: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type M_Room_Encryption = {
|
|
||||||
algorithm: string
|
|
||||||
rotation_period_ms?: number
|
|
||||||
rotation_period_msgs?: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace R {
|
export namespace R {
|
||||||
|
|
@ -443,7 +437,6 @@ export namespace R {
|
||||||
num_joined_members: number
|
num_joined_members: number
|
||||||
room_id: string
|
room_id: string
|
||||||
room_type?: string
|
room_type?: string
|
||||||
encryption?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ResolvedRoom = {
|
export type ResolvedRoom = {
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ block body
|
||||||
|
|
||||||
h3.mt32.fs-category Default roles
|
h3.mt32.fs-category Default roles
|
||||||
.s-card
|
.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
|
form(method="post" action=rel("/api/default-roles") hx-post=rel("/api/default-roles") 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)
|
input(type="hidden" name="guild_id" value=guild_id)
|
||||||
.d-flex.fw-wrap.g4
|
.d-flex.fw-wrap.g4
|
||||||
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
.s-tag.s-tag__md.fs-body1.s-tag__required @everyone
|
||||||
|
|
@ -249,11 +249,6 @@ block body
|
||||||
ul.my8.ml24
|
ul.my8.ml24
|
||||||
each row in removedLinkedRooms
|
each row in removedLinkedRooms
|
||||||
li: a(href=`https://matrix.to/#/${row.room_id}`)= row.name
|
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
|
h3.mt24 Unavailable rooms: Wrong type
|
||||||
.s-card.p0
|
.s-card.p0
|
||||||
ul.my8.ml24
|
ul.my8.ml24
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,6 @@ as.router.post("/api/default-roles", defineEventHandler(async event => {
|
||||||
db.prepare("INSERT OR IGNORE INTO role_default (guild_id, role_id) VALUES (?, ?)").run(guildID, 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")) {
|
if (getRequestHeader(event, "HX-Request")) {
|
||||||
return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
|
return pugSync.render(event, "fragments/default-roles-list.pug", {guild, guild_id: guildID})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -123,14 +123,13 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||||
let unlinkedRooms = [...rooms]
|
let unlinkedRooms = [...rooms]
|
||||||
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
let removedLinkedRooms = dUtils.filterTo(unlinkedRooms, r => !linkedRoomIDs.includes(r.room_id))
|
||||||
let removedWrongTypeRooms = dUtils.filterTo(unlinkedRooms, r => !r.room_type)
|
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
|
// 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
|
// 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(/^\[(🔒)?⛓️\]/))
|
let removedArchivedThreadRooms = dUtils.filterTo(unlinkedRooms, r => r.name && !r.name.match(/^\[(🔒)?⛓️\]/))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
linkedChannelsWithDetails, unlinkedChannels, unlinkedRooms,
|
||||||
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms, removedEncryptedRooms
|
removedUncachedChannels, removedWrongTypeChannels, removedPrivateChannels, removedLinkedRooms, removedWrongTypeRooms, removedArchivedThreadRooms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -204,12 +204,6 @@ as.router.post("/api/link", defineEventHandler(async event => {
|
||||||
throw createError({status: 403, message: e.errcode, data: `${e.errcode} - ${e.message}`})
|
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
|
// Check bridge has PL 100
|
||||||
const {powerLevels, powers: {[utils.bot]: selfPowerLevel}} = await utils.getEffectivePower(parsedBody.matrix, [utils.bot], api)
|
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"})
|
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"})
|
||||||
|
|
|
||||||
|
|
@ -435,47 +435,6 @@ test("web link room: check that bridge can join room (uses via for join attempt)
|
||||||
t.equal(called, 2)
|
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 => {
|
test("web link room: check that bridge has PL 100 in target room", async t => {
|
||||||
let called = 0
|
let called = 0
|
||||||
const [error] = await tryToCatch(() => router.test("post", "/api/link", {
|
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) {
|
async getStateEvent(roomID, type, key) {
|
||||||
called++
|
called++
|
||||||
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
t.equal(roomID, "!NDbIqNpJyPvfKRnNcr:cadence.moe")
|
||||||
if (type === "m.room.power_levels" && key === "") {
|
t.equal(type, "m.room.power_levels")
|
||||||
return {users_default: 50}
|
t.equal(key, "")
|
||||||
}
|
return {users_default: 50}
|
||||||
throw new Error("Unknown state event")
|
|
||||||
},
|
},
|
||||||
async getStateEventOuter(roomID, type, key) {
|
async getStateEventOuter(roomID, type, key) {
|
||||||
called++
|
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(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 => {
|
test("web link room: successfully calls createRoom", async t => {
|
||||||
|
|
|
||||||
135
test/data.js
135
test/data.js
|
|
@ -5067,141 +5067,6 @@ module.exports = {
|
||||||
pinned: false,
|
pinned: false,
|
||||||
mention_everyone: false,
|
mention_everyone: false,
|
||||||
tts: 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: {
|
message_with_components: {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue