Compare commits
2 commits
33eef25cf1
...
cd0b8bff2b
| Author | SHA1 | Date | |
|---|---|---|---|
| cd0b8bff2b | |||
| c4909653aa |
22 changed files with 134 additions and 65 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2024",
|
"target": "es2024",
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
|
"lib": ["ESNext"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"useUnknownInCatchVariables": false
|
"useUnknownInCatchVariables": false
|
||||||
|
|
|
||||||
17
scripts/reset-web-password.js
Normal file
17
scripts/reset-web-password.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const {reg, writeRegistration, registrationFilePath} = require("../src/matrix/read-registration")
|
||||||
|
const {prompt} = require("enquirer")
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
/** @type {{web_password: string}} */
|
||||||
|
const passwordResponse = await prompt({
|
||||||
|
type: "text",
|
||||||
|
name: "web_password",
|
||||||
|
message: "Choose a simple password (optional)"
|
||||||
|
})
|
||||||
|
|
||||||
|
reg.ooye.web_password = passwordResponse.web_password
|
||||||
|
writeRegistration(reg)
|
||||||
|
console.log("Saved. Restart Out Of Your Element to apply this change.")
|
||||||
|
})()
|
||||||
|
|
@ -70,13 +70,14 @@ async function sendVotes(userOrID, channelID, pollMessageID, pollEventID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let userID, senderMxid
|
||||||
if (typeof userOrID === "string") { // just a string when double-checking a vote removal - good thing the unvoter is already here from having voted
|
if (typeof userOrID === "string") { // just a string when double-checking a vote removal - good thing the unvoter is already here from having voted
|
||||||
var userID = userOrID
|
userID = userOrID
|
||||||
var senderMxid = from("sim").join("sim_member", "mxid").where({user_id: userOrID, room_id: matchingRoomID}).pluck("mxid").get()
|
senderMxid = from("sim").join("sim_member", "mxid").where({user_id: userOrID, room_id: matchingRoomID}).pluck("mxid").get()
|
||||||
if (!senderMxid) return
|
if (!senderMxid) return
|
||||||
} else { // sent in full when double-checking adding a vote, so we can properly ensure joined
|
} else { // sent in full when double-checking adding a vote, so we can properly ensure joined
|
||||||
var userID = userOrID.id
|
userID = userOrID.id
|
||||||
var senderMxid = await registerUser.ensureSimJoined(userOrID, matchingRoomID)
|
senderMxid = await registerUser.ensureSimJoined(userOrID, matchingRoomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
const answersArray = select("poll_vote", "matrix_option", {discord_or_matrix_user_id: userID, message_id: pollMessageID}).pluck().all()
|
const answersArray = select("poll_vote", "matrix_option", {discord_or_matrix_user_id: userID, message_id: pollMessageID}).pluck().all()
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const emitter = new EventEmitter()
|
||||||
* Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
* Due to Eventual Consistency(TM) an update/delete may arrive before the original message arrives
|
||||||
* (or before the it has finished being bridged to an event).
|
* (or before the it has finished being bridged to an event).
|
||||||
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
* In this case, wait until the original message has finished bridging, then retrigger the passed function.
|
||||||
* @template {(...args: any[]) => Promise<any>} T
|
* @template {(...args: any[]) => any} T
|
||||||
* @param {string} inputID
|
* @param {string} inputID
|
||||||
* @param {T} fn
|
* @param {T} fn
|
||||||
* @param {Parameters<T>} rest
|
* @param {Parameters<T>} rest
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ async function doSpeedbump(messageID) {
|
||||||
debugSpeedbump(`[speedbump] DELETED ${messageID}`)
|
debugSpeedbump(`[speedbump] DELETED ${messageID}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
value = bumping.get(messageID) - 1
|
value = (bumping.get(messageID) ?? 0) - 1
|
||||||
if (value === 0) {
|
if (value <= 0) {
|
||||||
debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`)
|
debugSpeedbump(`[speedbump] OK ${messageID}-- = ${value}`)
|
||||||
bumping.delete(messageID)
|
bumping.delete(messageID)
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const userRegex = reg.namespaces.users.map(u => new RegExp(u.regex))
|
||||||
* @typedef {{text: string, index: number, end: number}} Token
|
* @typedef {{text: string, index: number, end: number}} Token
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @typedef {{mxids: {localpart: string, mxid: string, displayname?: string}[], names: {displaynameTokens: Token[], mxid: string}[]}} ProcessedJoined */
|
/** @typedef {{mxids: {localpart: string, mxid: string, displayname?: string | null}[], names: {displaynameTokens: Token[], mxid: string}[]}} ProcessedJoined */
|
||||||
|
|
||||||
const lengthBonusLengthCap = 50
|
const lengthBonusLengthCap = 50
|
||||||
const lengthBonusValue = 0.5
|
const lengthBonusValue = 0.5
|
||||||
|
|
@ -18,7 +18,7 @@ const lengthBonusValue = 0.5
|
||||||
* 0 = no match
|
* 0 = no match
|
||||||
* @param {string} localpart
|
* @param {string} localpart
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
* @param {string} [displayname] only for the super tiebreaker
|
* @param {string | null} [displayname] only for the super tiebreaker
|
||||||
* @returns {{score: number, matchedInputTokens: Token[]}}
|
* @returns {{score: number, matchedInputTokens: Token[]}}
|
||||||
*/
|
*/
|
||||||
function scoreLocalpart(localpart, input, displayname) {
|
function scoreLocalpart(localpart, input, displayname) {
|
||||||
|
|
@ -103,7 +103,7 @@ function tokenise(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{mxid: string, displayname?: string}[]} joined
|
* @param {{mxid: string, displayname?: string | null}[]} joined
|
||||||
* @returns {ProcessedJoined}
|
* @returns {ProcessedJoined}
|
||||||
*/
|
*/
|
||||||
function processJoined(joined) {
|
function processJoined(joined) {
|
||||||
|
|
@ -120,6 +120,7 @@ function processJoined(joined) {
|
||||||
}),
|
}),
|
||||||
names: joined.filter(j => j.displayname).map(j => {
|
names: joined.filter(j => j.displayname).map(j => {
|
||||||
return {
|
return {
|
||||||
|
// @ts-ignore
|
||||||
displaynameTokens: tokenise(j.displayname),
|
displaynameTokens: tokenise(j.displayname),
|
||||||
mxid: j.mxid
|
mxid: j.mxid
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +131,8 @@ function processJoined(joined) {
|
||||||
/**
|
/**
|
||||||
* @param {ProcessedJoined} pjr
|
* @param {ProcessedJoined} pjr
|
||||||
* @param {string} maximumWrittenSection lowercase please
|
* @param {string} maximumWrittenSection lowercase please
|
||||||
|
* @param {number} baseOffset
|
||||||
|
* @param {string} prefix
|
||||||
* @param {string} content
|
* @param {string} content
|
||||||
*/
|
*/
|
||||||
function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
||||||
|
|
@ -142,7 +145,7 @@ function findMention(pjr, maximumWrittenSection, baseOffset, prefix, content) {
|
||||||
if (best.scored.score > 4) { // requires in smallest case perfect match of 2 characters, or in largest case a partial middle match of 5+ characters in a row
|
if (best.scored.score > 4) { // requires in smallest case perfect match of 2 characters, or in largest case a partial middle match of 5+ characters in a row
|
||||||
// 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.at(-1).end
|
const end = baseOffset + prefix.length + best.scored.matchedInputTokens.slice(-1)[0].end
|
||||||
const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end)
|
const newContent = content.slice(0, start) + "[" + content.slice(start, end) + "](https://matrix.to/#/" + best.mxid + ")" + content.slice(end)
|
||||||
return {
|
return {
|
||||||
mxid: best.mxid,
|
mxid: best.mxid,
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ test("score name: finds match location", t => {
|
||||||
const message = "evil lillith is an inspiration"
|
const message = "evil lillith is an inspiration"
|
||||||
const result = scoreName(tokenise("INX | Evil Lillith (she/her)"), tokenise(message))
|
const result = scoreName(tokenise("INX | Evil Lillith (she/her)"), tokenise(message))
|
||||||
const startLocation = result.matchedInputTokens[0].index
|
const startLocation = result.matchedInputTokens[0].index
|
||||||
const endLocation = result.matchedInputTokens.at(-1).end
|
const endLocation = result.matchedInputTokens.slice(-1)[0].end
|
||||||
t.equal(message.slice(startLocation, endLocation), "evil lillith")
|
t.equal(message.slice(startLocation, endLocation), "evil lillith")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -125,5 +125,5 @@ test("find mention: test various tiebreakers", t => {
|
||||||
mxid: "@emma:rory.gay",
|
mxid: "@emma:rory.gay",
|
||||||
displayname: "Emma [it/its]"
|
displayname: "Emma [it/its]"
|
||||||
}]), "emma ⚡ curious which one this prefers", 0, "@", "@emma ⚡ curious which one this prefers")
|
}]), "emma ⚡ curious which one this prefers", 0, "@", "@emma ⚡ curious which one this prefers")
|
||||||
t.equal(found.mxid, "@emma:conduit.rory.gay")
|
t.equal(found?.mxid, "@emma:conduit.rory.gay")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
* @param {string} [timestampChannelID]
|
* @param {string} [timestampChannelID]
|
||||||
*/
|
*/
|
||||||
async function getHistoricalEventRow(messageID, timestampChannelID) {
|
async function getHistoricalEventRow(messageID, timestampChannelID) {
|
||||||
/** @type {{room_id: string} | {event_id: string, room_id: string, reference_channel_id: string, source: number} | null} */
|
/** @type {{room_id: string} | {event_id: string, room_id: string, reference_channel_id: string, source: number} | null | undefined} */
|
||||||
let row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
let row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
.select("event_id", "room_id", "reference_channel_id", "source").where({message_id: messageID}).and("ORDER BY part ASC").get()
|
.select("event_id", "room_id", "reference_channel_id", "source").where({message_id: messageID}).and("ORDER BY part ASC").get()
|
||||||
if (!row && timestampChannelID) {
|
if (!row && timestampChannelID) {
|
||||||
|
|
@ -574,6 +574,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
if (repliedToEventInDifferentRoom || repliedToUnknownEvent) {
|
if (repliedToEventInDifferentRoom || repliedToUnknownEvent) {
|
||||||
let referenced = message.referenced_message
|
let referenced = message.referenced_message
|
||||||
if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves
|
if (!referenced) { // backend couldn't be bothered to dereference the message, have to do it ourselves
|
||||||
|
assert(message.message_reference?.message_id)
|
||||||
referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id)
|
referenced = await discord.snow.channel.getChannelMessage(message.message_reference.channel_id, message.message_reference.message_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -661,14 +662,14 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forwarded content appears first
|
// Forwarded content appears first
|
||||||
if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_snapshots?.length) {
|
if (message.message_reference?.type === DiscordTypes.MessageReferenceType.Forward && message.message_reference.message_id && message.message_snapshots?.length) {
|
||||||
// Forwarded notice
|
// Forwarded notice
|
||||||
const row = await getHistoricalEventRow(message.message_reference.message_id, message.message_reference.channel_id)
|
const row = await getHistoricalEventRow(message.message_reference.message_id, message.message_reference.channel_id)
|
||||||
const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get()
|
const room = select("channel_room", ["room_id", "name", "nick"], {channel_id: message.message_reference.channel_id}).get()
|
||||||
const forwardedNotice = new mxUtils.MatrixStringBuilder()
|
const forwardedNotice = new mxUtils.MatrixStringBuilder()
|
||||||
if (room) {
|
if (room) {
|
||||||
const roomName = room && (room.nick || room.name)
|
const roomName = room && (room.nick || room.name)
|
||||||
if ("event_id" in row) {
|
if (row && "event_id" in row) {
|
||||||
const via = await getViaServersMemo(row.room_id)
|
const via = await getViaServersMemo(row.room_id)
|
||||||
forwardedNotice.addLine(
|
forwardedNotice.addLine(
|
||||||
`[🔀 Forwarded from #${roomName}]`,
|
`[🔀 Forwarded from #${roomName}]`,
|
||||||
|
|
@ -802,20 +803,23 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
// Then components
|
// Then components
|
||||||
if (message.components?.length) {
|
if (message.components?.length) {
|
||||||
const stack = [new mxUtils.MatrixStringBuilder()]
|
const stack = new mxUtils.MatrixStringBuilderStack()
|
||||||
/** @param {DiscordTypes.APIMessageComponent} component */
|
/** @param {DiscordTypes.APIMessageComponent} component */
|
||||||
async function processComponent(component) {
|
async function processComponent(component) {
|
||||||
// Standalone components
|
// Standalone components
|
||||||
if (component.type === DiscordTypes.ComponentType.TextDisplay) {
|
if (component.type === DiscordTypes.ComponentType.TextDisplay) {
|
||||||
const {body, html} = await transformContent(component.content)
|
const {body, html} = await transformContent(component.content)
|
||||||
stack[0].addParagraph(body, html)
|
stack.msb.addParagraph(body, html)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.Separator) {
|
else if (component.type === DiscordTypes.ComponentType.Separator) {
|
||||||
stack[0].addParagraph("----", "<hr>")
|
stack.msb.addParagraph("----", "<hr>")
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.File) {
|
else if (component.type === DiscordTypes.ComponentType.File) {
|
||||||
const ev = await attachmentToEvent({}, {...component.file, filename: component.name, size: component.size}, true)
|
/** @type {{[k in keyof DiscordTypes.APIUnfurledMediaItem]-?: NonNullable<DiscordTypes.APIUnfurledMediaItem[k]>}} */ // @ts-ignore
|
||||||
stack[0].addLine(ev.body, ev.formatted_body)
|
const file = component.file
|
||||||
|
assert(component.name && component.size && file.content_type)
|
||||||
|
const ev = await attachmentToEvent({}, {...file, filename: component.name, size: component.size}, true)
|
||||||
|
stack.msb.addLine(ev.body, ev.formatted_body)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.MediaGallery) {
|
else if (component.type === DiscordTypes.ComponentType.MediaGallery) {
|
||||||
const description = component.items.length === 1 ? component.items[0].description || "Image:" : "Image gallery:"
|
const description = component.items.length === 1 ? component.items[0].description || "Image:" : "Image gallery:"
|
||||||
|
|
@ -826,43 +830,43 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
estimatedName: item.media.url.match(/\/([^/?]+)(\?|$)/)?.[1] || 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(", ")}`)
|
stack.msb.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
|
// string select, text input, user select, role select, mentionable select, channel select
|
||||||
|
|
||||||
// Components that can have things nested
|
// Components that can have things nested
|
||||||
else if (component.type === DiscordTypes.ComponentType.Container) {
|
else if (component.type === DiscordTypes.ComponentType.Container) {
|
||||||
// May contain action row, text display, section, media gallery, separator, file
|
// May contain action row, text display, section, media gallery, separator, file
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
stack.bump()
|
||||||
for (const innerComponent of component.components) {
|
for (const innerComponent of component.components) {
|
||||||
await processComponent(innerComponent)
|
await processComponent(innerComponent)
|
||||||
}
|
}
|
||||||
let {body, formatted_body} = stack.shift().get()
|
let {body, formatted_body} = stack.shift().get()
|
||||||
body = body.split("\n").map(l => "| " + l).join("\n")
|
body = body.split("\n").map(l => "| " + l).join("\n")
|
||||||
formatted_body = `<blockquote>${formatted_body}</blockquote>`
|
formatted_body = `<blockquote>${formatted_body}</blockquote>`
|
||||||
if (stack[0].body) stack[0].body += "\n\n"
|
if (stack.msb.body) stack.msb.body += "\n\n"
|
||||||
stack[0].add(body, formatted_body)
|
stack.msb.add(body, formatted_body)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.Section) {
|
else if (component.type === DiscordTypes.ComponentType.Section) {
|
||||||
// May contain text display, possibly more in the future
|
// May contain text display, possibly more in the future
|
||||||
// Accessory may be button or thumbnail
|
// Accessory may be button or thumbnail
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
stack.bump()
|
||||||
for (const innerComponent of component.components) {
|
for (const innerComponent of component.components) {
|
||||||
await processComponent(innerComponent)
|
await processComponent(innerComponent)
|
||||||
}
|
}
|
||||||
if (component.accessory) {
|
if (component.accessory) {
|
||||||
stack.unshift(new mxUtils.MatrixStringBuilder())
|
stack.bump()
|
||||||
await processComponent(component.accessory)
|
await processComponent(component.accessory)
|
||||||
const {body, formatted_body} = stack.shift().get()
|
const {body, formatted_body} = stack.shift().get()
|
||||||
stack[0].addLine(body, formatted_body)
|
stack.msb.addLine(body, formatted_body)
|
||||||
}
|
}
|
||||||
const {body, formatted_body} = stack.shift().get()
|
const {body, formatted_body} = stack.shift().get()
|
||||||
stack[0].addParagraph(body, formatted_body)
|
stack.msb.addParagraph(body, formatted_body)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.ActionRow) {
|
else if (component.type === DiscordTypes.ComponentType.ActionRow) {
|
||||||
const linkButtons = component.components.filter(c => c.type === DiscordTypes.ComponentType.Button && c.style === DiscordTypes.ButtonStyle.Link)
|
const linkButtons = component.components.filter(c => c.type === DiscordTypes.ComponentType.Button && c.style === DiscordTypes.ButtonStyle.Link)
|
||||||
if (linkButtons.length) {
|
if (linkButtons.length) {
|
||||||
stack[0].addLine("")
|
stack.msb.addLine("")
|
||||||
for (const linkButton of linkButtons) {
|
for (const linkButton of linkButtons) {
|
||||||
await processComponent(linkButton)
|
await processComponent(linkButton)
|
||||||
}
|
}
|
||||||
|
|
@ -871,15 +875,15 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
// Components that can only be inside things
|
// Components that can only be inside things
|
||||||
else if (component.type === DiscordTypes.ComponentType.Thumbnail) {
|
else if (component.type === DiscordTypes.ComponentType.Thumbnail) {
|
||||||
// May only be a section accessory
|
// May only be a section accessory
|
||||||
stack[0].add(`🖼️ ${component.media.url}`, tag`🖼️ <a href="${component.media.url}">${component.media.url}</a>`)
|
stack.msb.add(`🖼️ ${component.media.url}`, tag`🖼️ <a href="${component.media.url}">${component.media.url}</a>`)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.Button) {
|
else if (component.type === DiscordTypes.ComponentType.Button) {
|
||||||
// May only be a section accessory or in an action row (up to 5)
|
// May only be a section accessory or in an action row (up to 5)
|
||||||
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
if (component.style === DiscordTypes.ButtonStyle.Link) {
|
||||||
if (component.label) {
|
if (component.label) {
|
||||||
stack[0].add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
stack.msb.add(`[${component.label} ${component.url}] `, tag`<a href="${component.url}">${component.label}</a> `)
|
||||||
} else {
|
} else {
|
||||||
stack[0].add(component.url)
|
stack.msb.add(component.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -891,7 +895,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
await processComponent(component)
|
await processComponent(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {body, formatted_body} = stack[0].get()
|
const {body, formatted_body} = stack.msb.get()
|
||||||
if (body.trim().length) {
|
if (body.trim().length) {
|
||||||
await addTextEvent(body, formatted_body, "m.text")
|
await addTextEvent(body, formatted_body, "m.text")
|
||||||
}
|
}
|
||||||
|
|
@ -914,7 +918,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.video?.url && !embed.title && 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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -937,6 +941,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
const rep = new mxUtils.MatrixStringBuilder()
|
const rep = new mxUtils.MatrixStringBuilder()
|
||||||
|
|
||||||
if (isKlipyGIF) {
|
if (isKlipyGIF) {
|
||||||
|
assert(embed.video?.url)
|
||||||
rep.add("[GIF] ", "➿ ")
|
rep.add("[GIF] ", "➿ ")
|
||||||
if (embed.title) {
|
if (embed.title) {
|
||||||
rep.add(`${embed.title} ${embed.video.url}`, tag`<a href="${embed.video.url}">${embed.title}</a>`)
|
rep.add(`${embed.title} ${embed.video.url}`, tag`<a href="${embed.video.url}">${embed.title}</a>`)
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ module.exports = {
|
||||||
async MESSAGE_REACTION_ADD(client, data) {
|
async MESSAGE_REACTION_ADD(client, data) {
|
||||||
if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
|
if (data.user_id === client.user.id) return // m2d reactions are added by the discord bot user - do not reflect them back to matrix.
|
||||||
if (data.emoji.name === "❓" && select("event_message", "message_id", {message_id: data.message_id, source: 0, part: 0}).get()) { // source 0 = matrix
|
if (data.emoji.name === "❓" && select("event_message", "message_id", {message_id: data.message_id, source: 0, part: 0}).get()) { // source 0 = matrix
|
||||||
const guild_id = data.guild_id ?? client.channels.get(data.channel_id)["guild_id"]
|
const guild_id = data.guild_id ?? client.channels.get(data.channel_id)?.["guild_id"]
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
client.snow.channel.deleteReaction(data.channel_id, data.message_id, data.emoji.name).catch(() => {}),
|
client.snow.channel.deleteReaction(data.channel_id, data.message_id, data.emoji.name).catch(() => {}),
|
||||||
// @ts-ignore - this is all you need for it to do a matrix-side lookup
|
// @ts-ignore - this is all you need for it to do a matrix-side lookup
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,11 @@ async function _interact({guild_id, data}, {api}) {
|
||||||
// from Matrix
|
// from Matrix
|
||||||
const event = await api.getEvent(message.room_id, message.event_id)
|
const event = await api.getEvent(message.room_id, message.event_id)
|
||||||
const via = await utils.getViaServersQuery(message.room_id, api)
|
const via = await utils.getViaServersQuery(message.room_id, api)
|
||||||
const inChannels = discord.guildChannelMap.get(guild_id)
|
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
||||||
.map(cid => discord.channels.get(cid))
|
assert(channelsInGuild)
|
||||||
|
const inChannels = channelsInGuild
|
||||||
|
// @ts-ignore
|
||||||
|
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
||||||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||||
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid: event.sender}).get())
|
||||||
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
const matrixMember = select("member_cache", ["displayname", "avatar_url"], {room_id: message.room_id, mxid: event.sender}).get()
|
||||||
|
|
@ -67,7 +70,7 @@ async function _interact({guild_id, data}, {api}) {
|
||||||
author: {
|
author: {
|
||||||
name,
|
name,
|
||||||
url: `https://matrix.to/#/${event.sender}`,
|
url: `https://matrix.to/#/${event.sender}`,
|
||||||
icon_url: utils.getPublicUrlForMxc(matrixMember.avatar_url)
|
icon_url: utils.getPublicUrlForMxc(matrixMember?.avatar_url)
|
||||||
},
|
},
|
||||||
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
description: `This Matrix message was delivered to Discord by **Out Of Your Element**.\n[View on Matrix →](<https://matrix.to/#/${message.room_id}/${message.event_id}?${via}>)\n\n**User ID**: [${event.sender}](<https://matrix.to/#/${event.sender}>)`,
|
||||||
color: 0x0dbd8b,
|
color: 0x0dbd8b,
|
||||||
|
|
@ -96,7 +99,7 @@ async function dm(interaction) {
|
||||||
const channel = await discord.snow.user.createDirectMessageChannel(interaction.member.user.id)
|
const channel = await discord.snow.user.createDirectMessageChannel(interaction.member.user.id)
|
||||||
const response = await _interact(interaction, {api})
|
const response = await _interact(interaction, {api})
|
||||||
assert(response.type === DiscordTypes.InteractionResponseType.ChannelMessageWithSource)
|
assert(response.type === DiscordTypes.InteractionResponseType.ChannelMessageWithSource)
|
||||||
response.data.flags &= 0 // not ephemeral
|
response.data.flags = 0 & 0 // not ephemeral
|
||||||
await discord.snow.channel.createMessage(channel.id, response.data)
|
await discord.snow.channel.createMessage(channel.id, response.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ async function* _interactAutocomplete({data, channel}, {api}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check it was used in a bridged channel
|
// Check it was used in a bridged channel
|
||||||
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()
|
||||||
if (!roomID) return yield exit()
|
if (!roomID) return yield exit()
|
||||||
|
|
||||||
// Check we are in fact autocompleting the first option, the user
|
// Check we are in fact autocompleting the first option, the user
|
||||||
|
|
@ -58,9 +58,9 @@ async function* _interactAutocomplete({data, channel}, {api}) {
|
||||||
const displaynameMatches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL AND displayname LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
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
|
// prioritise matches closer to the start
|
||||||
displaynameMatches.sort((a, b) => {
|
displaynameMatches.sort((a, b) => {
|
||||||
let ai = a.displayname.toLowerCase().indexOf(input.toLowerCase())
|
let ai = a.displayname?.toLowerCase().indexOf(input.toLowerCase()) ?? -1
|
||||||
if (ai === -1) ai = 999
|
if (ai === -1) ai = 999
|
||||||
let bi = b.displayname.toLowerCase().indexOf(input.toLowerCase())
|
let bi = b.displayname?.toLowerCase().indexOf(input.toLowerCase()) ?? -1
|
||||||
if (bi === -1) bi = 999
|
if (bi === -1) bi = 999
|
||||||
return ai - bi
|
return ai - bi
|
||||||
})
|
})
|
||||||
|
|
@ -132,14 +132,18 @@ async function* _interactCommand({data, channel, guild_id}, {api}) {
|
||||||
type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource
|
type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
let member
|
||||||
try {
|
try {
|
||||||
/** @type {Ty.Event.M_Room_Member} */
|
/** @type {Ty.Event.M_Room_Member} */
|
||||||
var member = await api.getStateEvent(roomID, "m.room.member", mxid)
|
member = await api.getStateEvent(roomID, "m.room.member", mxid)
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
if (!member || member.membership !== "join") {
|
if (!member || member.membership !== "join") {
|
||||||
const inChannels = discord.guildChannelMap.get(guild_id)
|
const channelsInGuild = discord.guildChannelMap.get(guild_id)
|
||||||
.map(cid => discord.channels.get(cid))
|
assert(channelsInGuild)
|
||||||
|
const inChannels = channelsInGuild
|
||||||
|
// @ts-ignore
|
||||||
|
.map(/** @returns {DiscordTypes.APIGuildChannel} */ cid => discord.channels.get(cid))
|
||||||
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
.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())
|
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid}).get())
|
||||||
if (inChannels.length) {
|
if (inChannels.length) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
*/
|
*/
|
||||||
async function addReaction(event) {
|
async function addReaction(event) {
|
||||||
// Wait until the corresponding channel and message have already been bridged
|
// Wait until the corresponding channel and message have already been bridged
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, as.emit.bind(as, "type:m.reaction", event))) return
|
if (retrigger.eventNotFoundThenRetrigger(event.content["m.relates_to"].event_id, () => as.emit("type:m.reaction", event))) return
|
||||||
|
|
||||||
// These will exist because it passed retrigger
|
// These will exist because it passed retrigger
|
||||||
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
const row = from("event_message").join("message_room", "message_id").join("historical_channel_room", "historical_room_index")
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ async function handle(event) {
|
||||||
await removeReaction(event)
|
await removeReaction(event)
|
||||||
|
|
||||||
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
// Or, it might be for removing a message or suppressing embeds. But to do that, the message needs to be bridged first.
|
||||||
if (retrigger.eventNotFoundThenRetrigger(event.redacts, as.emit.bind(as, "type:m.room.redaction", event))) return
|
if (retrigger.eventNotFoundThenRetrigger(event.redacts, () => as.emit("type:m.room.redaction", event))) return
|
||||||
|
|
||||||
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
const row = select("event_message", ["event_type", "event_subtype", "part"], {event_id: event.redacts}).get()
|
||||||
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
if (row && row.event_type === "m.room.message" && row.event_subtype === "m.notice" && row.part === 1) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
/// <reference lib="dom" />
|
||||||
|
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
|
@ -371,6 +372,7 @@ function linkEndOfMessageSpriteSheet(content) {
|
||||||
for (const mxc of endOfMessageEmojis) {
|
for (const mxc of endOfMessageEmojis) {
|
||||||
// We can do up to 2000 chars max. (In this maximal case it will get chunked to a separate message.) Ignore additional emojis.
|
// We can do up to 2000 chars max. (In this maximal case it will get chunked to a separate message.) Ignore additional emojis.
|
||||||
const withoutMxc = mxUtils.makeMxcPublic(mxc)
|
const withoutMxc = mxUtils.makeMxcPublic(mxc)
|
||||||
|
assert(withoutMxc)
|
||||||
const emojisLength = params.toString().length + encodeURIComponent(withoutMxc).length + 2
|
const emojisLength = params.toString().length + encodeURIComponent(withoutMxc).length + 2
|
||||||
if (content.length + emojisLength + afterLink.length > 2000) {
|
if (content.length + emojisLength + afterLink.length > 2000) {
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -196,9 +196,10 @@ async function getInviteState(roomID, event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try calling sliding sync API and extracting from stripped state
|
// Try calling sliding sync API and extracting from stripped state
|
||||||
|
let root
|
||||||
try {
|
try {
|
||||||
/** @type {Ty.R.SSS} */
|
/** @type {Ty.R.SSS} */
|
||||||
var root = await mreq.mreq("POST", path("/client/unstable/org.matrix.simplified_msc3575/sync", `@${reg.sender_localpart}:${reg.ooye.server_name}`, {timeout: "0"}), {
|
root = await mreq.mreq("POST", path("/client/unstable/org.matrix.simplified_msc3575/sync", `@${reg.sender_localpart}:${reg.ooye.server_name}`, {timeout: "0"}), {
|
||||||
lists: {
|
lists: {
|
||||||
a: {
|
a: {
|
||||||
ranges: [[0, 999]],
|
ranges: [[0, 999]],
|
||||||
|
|
@ -239,7 +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
|
type: room.room_type ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -426,7 +427,7 @@ async function profileSetDisplayname(mxid, displayname, inhibitPropagate) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} mxid
|
* @param {string} mxid
|
||||||
* @param {string} avatar_url
|
* @param {string | null | undefined} avatar_url
|
||||||
* @param {boolean} [inhibitPropagate]
|
* @param {boolean} [inhibitPropagate]
|
||||||
*/
|
*/
|
||||||
async function profileSetAvatarUrl(mxid, avatar_url, inhibitPropagate) {
|
async function profileSetAvatarUrl(mxid, avatar_url, inhibitPropagate) {
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ const commands = [{
|
||||||
if (matrixOnlyReason) {
|
if (matrixOnlyReason) {
|
||||||
// If uploading to Matrix, check if we have permission
|
// If uploading to Matrix, check if we have permission
|
||||||
const {powerLevels, powers: {[mxUtils.bot]: botPower}} = await mxUtils.getEffectivePower(event.room_id, [mxUtils.bot], api)
|
const {powerLevels, powers: {[mxUtils.bot]: botPower}} = await mxUtils.getEffectivePower(event.room_id, [mxUtils.bot], api)
|
||||||
const requiredPower = powerLevels.events["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
|
const requiredPower = powerLevels.events?.["im.ponies.room_emotes"] ?? powerLevels.state_default ?? 50
|
||||||
if (botPower < requiredPower) {
|
if (botPower < requiredPower) {
|
||||||
return api.sendEvent(event.room_id, "m.room.message", {
|
return api.sendEvent(event.room_id, "m.room.message", {
|
||||||
...ctx,
|
...ctx,
|
||||||
|
|
|
||||||
|
|
@ -57,12 +57,12 @@ async function onBotMembership(event, api, createRoom) {
|
||||||
// Check if an upgrade is pending for this room
|
// Check if an upgrade is pending for this room
|
||||||
const newRoomID = event.room_id
|
const newRoomID = event.room_id
|
||||||
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
const oldRoomID = select("room_upgrade_pending", "old_room_id", {new_room_id: newRoomID}).pluck().get()
|
||||||
if (!oldRoomID) return
|
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()
|
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
|
assert(channelRow) // this could only fail if the channel was unbridged or something between upgrade and joining
|
||||||
|
|
||||||
// Check if is join/invite
|
// Check if is join/invite
|
||||||
if (event.content.membership !== "invite" && event.content.membership !== "join") return
|
if (event.content.membership !== "invite" && event.content.membership !== "join") return false
|
||||||
|
|
||||||
return await roomUpgradeSema.request(async () => {
|
return await roomUpgradeSema.request(async () => {
|
||||||
// If invited, join
|
// If invited, join
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,26 @@ function getEventIDHash(eventID) {
|
||||||
return signedHash
|
return signedHash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MatrixStringBuilderStack {
|
||||||
|
constructor() {
|
||||||
|
this.stack = [new MatrixStringBuilder()]
|
||||||
|
}
|
||||||
|
|
||||||
|
get msb() {
|
||||||
|
return this.stack[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
bump() {
|
||||||
|
this.stack.unshift(new MatrixStringBuilder())
|
||||||
|
}
|
||||||
|
|
||||||
|
shift() {
|
||||||
|
const msb = this.stack.shift()
|
||||||
|
assert(msb)
|
||||||
|
return msb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MatrixStringBuilder {
|
class MatrixStringBuilder {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.body = ""
|
this.body = ""
|
||||||
|
|
@ -228,7 +248,7 @@ function generatePermittedMediaHash(mxc) {
|
||||||
* @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background
|
* @see https://matrix.org/blog/2024/06/26/sunsetting-unauthenticated-media/ background
|
||||||
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
|
* @see https://matrix.org/blog/2024/06/20/matrix-v1.11-release/ implementation details
|
||||||
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
|
* @see https://www.sqlite.org/fileformat2.html#record_format SQLite integer field size
|
||||||
* @param {string} mxc
|
* @param {string | null | undefined} mxc
|
||||||
* @returns {string | undefined}
|
* @returns {string | undefined}
|
||||||
*/
|
*/
|
||||||
function getPublicUrlForMxc(mxc) {
|
function getPublicUrlForMxc(mxc) {
|
||||||
|
|
@ -238,7 +258,7 @@ function getPublicUrlForMxc(mxc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} mxc
|
* @param {string | null | undefined} mxc
|
||||||
* @returns {string | undefined} mxc URL with protocol stripped, e.g. "cadence.moe/abcdef1234"
|
* @returns {string | undefined} mxc URL with protocol stripped, e.g. "cadence.moe/abcdef1234"
|
||||||
*/
|
*/
|
||||||
function makeMxcPublic(mxc) {
|
function makeMxcPublic(mxc) {
|
||||||
|
|
@ -289,7 +309,7 @@ function roomHasAtLeastVersion(roomVersionString, desiredVersion) {
|
||||||
*/
|
*/
|
||||||
function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) {
|
function removeCreatorsFromPowerLevels(roomCreateOuter, powerLevels) {
|
||||||
assert(roomCreateOuter.sender)
|
assert(roomCreateOuter.sender)
|
||||||
if (roomHasAtLeastVersion(roomCreateOuter.content.room_version, 12)) {
|
if (roomHasAtLeastVersion(roomCreateOuter.content.room_version, 12) && powerLevels.users) {
|
||||||
for (const creator of (roomCreateOuter.content.additional_creators ?? []).concat(roomCreateOuter.sender)) {
|
for (const creator of (roomCreateOuter.content.additional_creators ?? []).concat(roomCreateOuter.sender)) {
|
||||||
delete powerLevels.users[creator]
|
delete powerLevels.users[creator]
|
||||||
}
|
}
|
||||||
|
|
@ -385,6 +405,7 @@ module.exports.makeMxcPublic = makeMxcPublic
|
||||||
module.exports.getPublicUrlForMxc = getPublicUrlForMxc
|
module.exports.getPublicUrlForMxc = getPublicUrlForMxc
|
||||||
module.exports.getEventIDHash = getEventIDHash
|
module.exports.getEventIDHash = getEventIDHash
|
||||||
module.exports.MatrixStringBuilder = MatrixStringBuilder
|
module.exports.MatrixStringBuilder = MatrixStringBuilder
|
||||||
|
module.exports.MatrixStringBuilderStack = MatrixStringBuilderStack
|
||||||
module.exports.getViaServers = getViaServers
|
module.exports.getViaServers = getViaServers
|
||||||
module.exports.getViaServersQuery = getViaServersQuery
|
module.exports.getViaServersQuery = getViaServersQuery
|
||||||
module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion
|
module.exports.roomHasAtLeastVersion = roomHasAtLeastVersion
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function getSnow(event) {
|
||||||
/** @type {Map<string, Promise<string>>} */
|
/** @type {Map<string, Promise<string>>} */
|
||||||
const cache = new Map()
|
const cache = new Map()
|
||||||
|
|
||||||
/** @param {string | undefined} url */
|
/** @param {string} url */
|
||||||
function timeUntilExpiry(url) {
|
function timeUntilExpiry(url) {
|
||||||
const params = new URL(url).searchParams
|
const params = new URL(url).searchParams
|
||||||
const ex = params.get("ex")
|
const ex = params.get("ex")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const {convertImageStream} = require("../../m2d/converters/emoji-sheet")
|
||||||
const tryToCatch = require("try-to-catch")
|
const tryToCatch = require("try-to-catch")
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {router} = require("../../../test/web")
|
const {router} = require("../../../test/web")
|
||||||
|
const streamWeb = require("stream/web")
|
||||||
|
|
||||||
test("web download matrix: access denied if not a known attachment", async t => {
|
test("web download matrix: access denied if not a known attachment", async t => {
|
||||||
const [error] = await tryToCatch(() =>
|
const [error] = await tryToCatch(() =>
|
||||||
|
|
@ -27,6 +28,7 @@ test("web download matrix: works if a known attachment", async t => {
|
||||||
},
|
},
|
||||||
event,
|
event,
|
||||||
api: {
|
api: {
|
||||||
|
// @ts-ignore
|
||||||
async getMedia(mxc, init) {
|
async getMedia(mxc, init) {
|
||||||
return new Response("", {status: 200, headers: {"content-type": "image/png"}})
|
return new Response("", {status: 200, headers: {"content-type": "image/png"}})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ function getAPI(event) {
|
||||||
const validNonce = new LRUCache({max: 200})
|
const validNonce = new LRUCache({max: 200})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{type: number, parent_id?: string, position?: number}} channel
|
* @param {{type: number, parent_id?: string | null, position?: number}} channel
|
||||||
* @param {Map<string, {type: number, parent_id?: string, position?: number}>} channels
|
* @param {Map<string, {type: number, parent_id?: string | null, position?: number}>} channels
|
||||||
*/
|
*/
|
||||||
function getPosition(channel, channels) {
|
function getPosition(channel, channels) {
|
||||||
let position = 0
|
let position = 0
|
||||||
|
|
@ -65,9 +65,11 @@ function getPosition(channel, channels) {
|
||||||
// Categories are size 2000.
|
// Categories are size 2000.
|
||||||
let foundCategory = channel
|
let foundCategory = channel
|
||||||
while (foundCategory.parent_id) {
|
while (foundCategory.parent_id) {
|
||||||
foundCategory = channels.get(foundCategory.parent_id)
|
const f = channels.get(foundCategory.parent_id)
|
||||||
|
assert(f)
|
||||||
|
foundCategory = f
|
||||||
}
|
}
|
||||||
if (foundCategory.type === DiscordTypes.ChannelType.GuildCategory) position = (foundCategory.position + 1) * 2000
|
if (foundCategory.type === DiscordTypes.ChannelType.GuildCategory) position = ((foundCategory.position || 0) + 1) * 2000
|
||||||
|
|
||||||
// Categories always appear above what they contain.
|
// Categories always appear above what they contain.
|
||||||
if (channel.type === DiscordTypes.ChannelType.GuildCategory) position -= 0.5
|
if (channel.type === DiscordTypes.ChannelType.GuildCategory) position -= 0.5
|
||||||
|
|
@ -81,7 +83,7 @@ function getPosition(channel, channels) {
|
||||||
// Threads appear below their channel.
|
// Threads appear below their channel.
|
||||||
if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
|
if ([DiscordTypes.ChannelType.PublicThread, DiscordTypes.ChannelType.PrivateThread, DiscordTypes.ChannelType.AnnouncementThread].includes(channel.type)) {
|
||||||
position += 0.5
|
position += 0.5
|
||||||
let parent = channels.get(channel.parent_id)
|
let parent = channels.get(channel.parent_id || "")
|
||||||
if (parent && parent["position"]) position += parent["position"]
|
if (parent && parent["position"]) position += parent["position"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +100,11 @@ function getChannelRoomsLinks(guild, rooms, roles) {
|
||||||
assert(channelIDs)
|
assert(channelIDs)
|
||||||
|
|
||||||
let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all()
|
let linkedChannels = select("channel_room", ["channel_id", "room_id", "name", "nick"], {channel_id: channelIDs}).all()
|
||||||
let linkedChannelsWithDetails = linkedChannels.map(c => ({channel: discord.channels.get(c.channel_id), ...c}))
|
let linkedChannelsWithDetails = linkedChannels.map(c => ({
|
||||||
|
// @ts-ignore
|
||||||
|
/** @type {DiscordTypes.APIGuildChannel} */ channel: discord.channels.get(c.channel_id),
|
||||||
|
...c
|
||||||
|
}))
|
||||||
let removedUncachedChannels = dUtils.filterTo(linkedChannelsWithDetails, c => c.channel)
|
let removedUncachedChannels = dUtils.filterTo(linkedChannelsWithDetails, c => c.channel)
|
||||||
let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id)
|
let linkedChannelIDs = linkedChannelsWithDetails.map(c => c.channel_id)
|
||||||
linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels))
|
linkedChannelsWithDetails.sort((a, b) => getPosition(a.channel, discord.channels) - getPosition(b.channel, discord.channels))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert").strict
|
||||||
const {z} = require("zod")
|
const {z} = require("zod")
|
||||||
const {defineEventHandler, createError, readValidatedBody, setResponseHeader, H3Event} = require("h3")
|
const {defineEventHandler, createError, readValidatedBody, setResponseHeader, H3Event} = require("h3")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
|
@ -77,7 +78,9 @@ as.router.post("/api/link-space", defineEventHandler(async event => {
|
||||||
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
|
const existing = select("guild_space", "guild_id", {}, "WHERE guild_id = ? OR space_id = ?").get(guildID, spaceID)
|
||||||
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
|
if (existing) throw createError({status: 400, message: "Bad Request", data: `Guild ID ${guildID} or space ID ${spaceID} are already bridged and cannot be reused`})
|
||||||
|
|
||||||
const via = [inviteRow.mxid.match(/:(.*)/)[1]]
|
const inviteServer = inviteRow.mxid.match(/:(.*)/)?.[1]
|
||||||
|
assert(inviteServer)
|
||||||
|
const via = [inviteServer]
|
||||||
|
|
||||||
// Check space exists and bridge is joined
|
// Check space exists and bridge is joined
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue