Compare commits
6 commits
move-reply
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 021f02f981 | |||
| c71120e463 | |||
| 965fcab646 | |||
| cf95f7d915 | |||
| 5e9f3cd532 | |||
| 7faa8a3c9e |
14 changed files with 189 additions and 63 deletions
|
|
@ -8,6 +8,10 @@ Modern Matrix-to-Discord appservice bridge, created by [@cadence:cadence.moe](ht
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## AI policy
|
||||||
|
|
||||||
|
Out Of Your Element is 100% organically cultivated ethical hand-fed code. Let's work together to conserve that!
|
||||||
|
|
||||||
## Why a new bridge?
|
## Why a new bridge?
|
||||||
|
|
||||||
* Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership.
|
* Modern: Supports new Discord features like replies, threads and stickers, and new Matrix features like edits, spaces and space membership.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
const assert = require("assert").strict
|
const assert = require("assert").strict
|
||||||
const {reg} = require("../../matrix/read-registration")
|
const {reg} = require("../../matrix/read-registration")
|
||||||
const Ty = require("../../types")
|
const Ty = require("../../types")
|
||||||
|
const {scheduler} = require("timers/promises")
|
||||||
|
|
||||||
const passthrough = require("../../passthrough")
|
const passthrough = require("../../passthrough")
|
||||||
const {sync, db, select, from} = passthrough
|
const {sync, db, select, from} = passthrough
|
||||||
|
|
@ -14,14 +15,21 @@ const file = sync.require("../../matrix/file")
|
||||||
const registerUser = sync.require("./register-user")
|
const registerUser = sync.require("./register-user")
|
||||||
|
|
||||||
/** @returns {Promise<Ty.PkMessage>} */
|
/** @returns {Promise<Ty.PkMessage>} */
|
||||||
async function fetchMessage(messageID) {
|
async function fetchMessage(messageID, attempt = 1) {
|
||||||
try {
|
try {
|
||||||
var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`)
|
var res = await fetch(`https://api.pluralkit.me/v2/messages/${messageID}`)
|
||||||
} catch (networkError) {
|
} catch (networkError) {
|
||||||
// Network issue, raise a more readable message
|
// Network issue, raise a more readable message
|
||||||
throw new Error(`Failed to connect to PK API: ${networkError.toString()}`)
|
throw new Error(`Failed to connect to PK API: ${networkError.toString()}`)
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`PK API returned an error: ${await res.text()}`)
|
if (!res.ok) {
|
||||||
|
if (attempt < 2) {
|
||||||
|
await scheduler.wait(5000)
|
||||||
|
return fetchMessage(messageID, attempt + 1)
|
||||||
|
} else {
|
||||||
|
throw new Error(`PK API returned an error: ${await res.text()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
/** @type {any} */
|
/** @type {any} */
|
||||||
const root = await res.json()
|
const root = await res.json()
|
||||||
if (!root.member) throw new Error(`PK API didn't return member data: ${JSON.stringify(root)}`)
|
if (!root.member) throw new Error(`PK API didn't return member data: ${JSON.stringify(root)}`)
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ function waitFor(id, resolve, existsInDatabase) {
|
||||||
const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw()
|
const GET_EVENT_PREPARED = from("event_message").select("event_id").and("WHERE event_id = ?").prepare().raw()
|
||||||
/**
|
/**
|
||||||
* @param {string} eventID
|
* @param {string} eventID
|
||||||
* @returns {Promise<boolean>} if true then the message did not arrive
|
* @returns {Promise<boolean>} if false then the message did not arrive
|
||||||
*/
|
*/
|
||||||
function waitForEvent(eventID) {
|
function waitForEvent(eventID) {
|
||||||
const {promise, resolve} = Promise.withResolvers()
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
|
@ -109,7 +109,7 @@ function waitForEvent(eventID) {
|
||||||
const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw()
|
const GET_MESSAGE_PREPARED = from("event_message").select("message_id").and("WHERE message_id = ?").prepare().raw()
|
||||||
/**
|
/**
|
||||||
* @param {string} messageID
|
* @param {string} messageID
|
||||||
* @returns {Promise<boolean>} if true then the message did not arrive
|
* @returns {Promise<boolean>} if false then the message did not arrive
|
||||||
*/
|
*/
|
||||||
function waitForMessage(messageID) {
|
function waitForMessage(messageID) {
|
||||||
const {promise, resolve} = Promise.withResolvers()
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
|
@ -120,7 +120,7 @@ function waitForMessage(messageID) {
|
||||||
const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw()
|
const GET_REACTION_EVENT_PREPARED = from("reaction").select("hashed_event_id").and("WHERE hashed_event_id = ?").prepare().raw()
|
||||||
/**
|
/**
|
||||||
* @param {string} eventID
|
* @param {string} eventID
|
||||||
* @returns {Promise<boolean>} if true then the message did not arrive
|
* @returns {Promise<boolean>} if false then the message did not arrive
|
||||||
*/
|
*/
|
||||||
function waitForReactionEvent(eventID) {
|
function waitForReactionEvent(eventID) {
|
||||||
const {promise, resolve} = Promise.withResolvers()
|
const {promise, resolve} = Promise.withResolvers()
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ async function sendMessage(message, channel, guild) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = await messageToEvent.messageToEvent(message, guild, {}, {api, snow: discord.snow})
|
const events = await messageToEvent.messageToEvent(message, guild, {}, {api, snow: discord.snow, fetch})
|
||||||
const eventIDs = []
|
const eventIDs = []
|
||||||
if (events.length) {
|
if (events.length) {
|
||||||
db.prepare("INSERT OR IGNORE INTO message_room (message_id, historical_room_index) VALUES (?, ?)").run(message.id, historicalRoomIndex)
|
db.prepare("INSERT OR IGNORE INTO message_room (message_id, historical_room_index) VALUES (?, ?)").run(message.id, historicalRoomIndex)
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ async function editToChanges(message, guild, api) {
|
||||||
|
|
||||||
// Figure out what we will be replacing them with
|
// Figure out what we will be replacing them with
|
||||||
|
|
||||||
const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api})
|
const newFallbackContent = await messageToEvent.messageToEvent(message, guild, {includeEditFallbackStar: true}, {api, fetch})
|
||||||
const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api})
|
const newInnerContent = await messageToEvent.messageToEvent(message, guild, {includeReplyFallback: false}, {api, fetch})
|
||||||
assert.ok(newFallbackContent.length === newInnerContent.length)
|
assert.ok(newFallbackContent.length === newInnerContent.length)
|
||||||
|
|
||||||
// Match the new events to the old events
|
// Match the new events to the old events
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,11 @@ const embedTitleParser = markdown.markdownEngine.parserFor({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
* @param {{room?: boolean, user_ids?: string[]}} mentions
|
||||||
* @param {Omit<DiscordTypes.APIAttachment, "id" | "proxy_url">} attachment
|
* @param {Omit<DiscordTypes.APIAttachment, "id" | "proxy_url"> & {proxy_url?: string}} attachment
|
||||||
* @param {boolean} [alwaysLink]
|
* @param {boolean} [alwaysLink]
|
||||||
|
* @param {{fetch?: typeof fetch}} [di]
|
||||||
*/
|
*/
|
||||||
async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
async function attachmentToEvent(mentions, attachment, alwaysLink, di) {
|
||||||
const external_url = dUtils.getPublicUrlForCdn(attachment.url)
|
const external_url = dUtils.getPublicUrlForCdn(attachment.url)
|
||||||
const emoji =
|
const emoji =
|
||||||
attachment.content_type?.startsWith("image/jp") ? "📸"
|
attachment.content_type?.startsWith("image/jp") ? "📸"
|
||||||
|
|
@ -132,8 +133,37 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
formatted_body: `<blockquote>${emoji} Uploaded SPOILER file: <a href="${external_url}">${external_url}</a> (${pb(attachment.size)})</blockquote>`
|
formatted_body: `<blockquote>${emoji} Uploaded SPOILER file: <a href="${external_url}">${external_url}</a> (${pb(attachment.size)})</blockquote>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// attempt to convert large PNG image files to JPEG, since it's almost certainly a photo that was forced into PNG by the user copy-pasting.
|
||||||
|
if (
|
||||||
|
attachment.content_type === "image/png" && attachment.size > reg.ooye.max_file_size
|
||||||
|
&& !alwaysLink && attachment.proxy_url && attachment.width && attachment.height && di?.fetch
|
||||||
|
) {
|
||||||
|
const proxyUrl = new URL(attachment.proxy_url)
|
||||||
|
proxyUrl.searchParams.set("format", "jpeg")
|
||||||
|
const jpegUrl = proxyUrl.toString()
|
||||||
|
const res = await di.fetch(jpegUrl, {method: "HEAD"})
|
||||||
|
const newFilename = attachment.filename.replace(/\.png$/, ".jpg")
|
||||||
|
const newSize = Number(res.headers.get("content-length"))
|
||||||
|
if (res.ok && res.headers.get("content-type") === "image/jpeg" && !res.headers.has("content-encoding") && newSize <= reg.ooye.max_file_size) {
|
||||||
|
return {
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": mentions,
|
||||||
|
msgtype: "m.image",
|
||||||
|
url: await file.uploadDiscordFileToMxc(jpegUrl),
|
||||||
|
external_url,
|
||||||
|
body: attachment.description || newFilename,
|
||||||
|
filename: newFilename,
|
||||||
|
info: {
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
w: attachment.width,
|
||||||
|
h: attachment.height,
|
||||||
|
size: newSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
|
// for large files, always link them instead of uploading so I don't use up all the space in the content repo
|
||||||
else if (alwaysLink || attachment.size > reg.ooye.max_file_size) {
|
if (alwaysLink || attachment.size > reg.ooye.max_file_size) {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -142,7 +172,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
formatted_body: `${emoji} Uploaded file: <a href="${external_url}">${attachment.filename}</a> (${pb(attachment.size)})`
|
formatted_body: `${emoji} Uploaded file: <a href="${external_url}">${attachment.filename}</a> (${pb(attachment.size)})`
|
||||||
}
|
}
|
||||||
} else if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) {
|
}
|
||||||
|
if (attachment.content_type?.startsWith("image/") && attachment.width && attachment.height) {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -158,7 +189,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
size: attachment.size
|
size: attachment.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) {
|
}
|
||||||
|
if (attachment.content_type?.startsWith("video/") && attachment.width && attachment.height) {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -174,7 +206,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
size: attachment.size
|
size: attachment.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (attachment.content_type?.startsWith("audio/")) {
|
}
|
||||||
|
if (attachment.content_type?.startsWith("audio/")) {
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -189,7 +222,8 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
duration: attachment.duration_secs && Math.round(attachment.duration_secs * 1000)
|
duration: attachment.duration_secs && Math.round(attachment.duration_secs * 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
// else
|
||||||
return {
|
return {
|
||||||
$type: "m.room.message",
|
$type: "m.room.message",
|
||||||
"m.mentions": mentions,
|
"m.mentions": mentions,
|
||||||
|
|
@ -204,7 +238,6 @@ async function attachmentToEvent(mentions, attachment, alwaysLink) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIPoll} poll */
|
/** @param {DiscordTypes.APIPoll} poll */
|
||||||
async function pollToEvent(poll) {
|
async function pollToEvent(poll) {
|
||||||
|
|
@ -295,7 +328,7 @@ function mergeTextEvents(newEvents, events, forceSameMsgtype, forceMerge = false
|
||||||
* - includeEditFallbackStar: false
|
* - includeEditFallbackStar: false
|
||||||
* - alwaysReturnFormattedBody: false - formatted_body will be skipped if it is the same as body because the message is plaintext. if you want the formatted_body to be returned anyway, for example to merge it with another message, then set this to true.
|
* - alwaysReturnFormattedBody: false - formatted_body will be skipped if it is the same as body because the message is plaintext. if you want the formatted_body to be returned anyway, for example to merge it with another message, then set this to true.
|
||||||
* - scanTextForMentions: true - needs to be set to false when converting forwarded messages etc which may be from a different channel that can't be scanned.
|
* - scanTextForMentions: true - needs to be set to false when converting forwarded messages etc which may be from a different channel that can't be scanned.
|
||||||
* @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer}} di simple-as-nails dependency injection for the matrix API
|
* @param {{api: import("../../matrix/api"), snow?: import("snowtransfer").SnowTransfer, fetch?: typeof fetch}} di simple-as-nails dependency injection for the matrix API
|
||||||
* @returns {Promise<{$type: string, $sender?: string, [x: string]: any}[]>}
|
* @returns {Promise<{$type: string, $sender?: string, [x: string]: any}[]>}
|
||||||
*/
|
*/
|
||||||
async function messageToEvent(message, guild, options = {}, di) {
|
async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
@ -882,7 +915,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
|
|
||||||
// Then attachments
|
// Then attachments
|
||||||
if (message.attachments) {
|
if (message.attachments) {
|
||||||
const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment)))
|
const attachmentEvents = await Promise.all(message.attachments.map(attachment => attachmentToEvent(mentions, attachment, false, {fetch: di?.fetch})))
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
@ -913,7 +946,7 @@ async function messageToEvent(message, guild, options = {}, di) {
|
||||||
url: file.url,
|
url: file.url,
|
||||||
height: file.height,
|
height: file.height,
|
||||||
width: file.width,
|
width: file.width,
|
||||||
}, true)
|
}, true, {fetch: di?.fetch})
|
||||||
stack.msb.addLine(ev.body, ev.formatted_body)
|
stack.msb.addLine(ev.body, ev.formatted_body)
|
||||||
}
|
}
|
||||||
else if (component.type === DiscordTypes.ComponentType.MediaGallery) {
|
else if (component.type === DiscordTypes.ComponentType.MediaGallery) {
|
||||||
|
|
|
||||||
|
|
@ -1103,6 +1103,45 @@ test("message2event: very large attachment is linked instead of being uploaded",
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("message2event: very large png is converted to jpeg for upload", async t => {
|
||||||
|
const events = await messageToEvent({
|
||||||
|
content: "",
|
||||||
|
attachments: [{
|
||||||
|
filename: "855064e9-bdae-487a-a497-c662bd510487.png",
|
||||||
|
url: "https://cdn.discordapp.com/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png",
|
||||||
|
proxy_url: "https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png",
|
||||||
|
content_type: "image/png",
|
||||||
|
width: 5472,
|
||||||
|
height: 3648,
|
||||||
|
size: 14967919
|
||||||
|
}]
|
||||||
|
}, data.guild.general, {}, {
|
||||||
|
async fetch(url, init) {
|
||||||
|
t.equal(url, "https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg")
|
||||||
|
t.equal(init.method, "HEAD")
|
||||||
|
return new Response("wa", {headers: {
|
||||||
|
"Content-Type": "image/jpeg",
|
||||||
|
"Content-Length": 483085
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.deepEqual(events, [{
|
||||||
|
$type: "m.room.message",
|
||||||
|
"m.mentions": {},
|
||||||
|
msgtype: "m.image",
|
||||||
|
body: "855064e9-bdae-487a-a497-c662bd510487.jpg",
|
||||||
|
info: {
|
||||||
|
w: 5472,
|
||||||
|
h: 3648,
|
||||||
|
mimetype: "image/jpeg",
|
||||||
|
size: 483085
|
||||||
|
},
|
||||||
|
external_url: "https://bridge.example.org/download/discordcdn/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png",
|
||||||
|
filename: "855064e9-bdae-487a-a497-c662bd510487.jpg",
|
||||||
|
url: "mxc://cadence.moe/zXNXRvJwRDMIzZZVqHUXtGeD"
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
test("message2event: multiple attachments are combined into the same event where possible", async t => {
|
test("message2event: multiple attachments are combined into the same event where possible", async t => {
|
||||||
const events = await messageToEvent({
|
const events = await messageToEvent({
|
||||||
content: "hey",
|
content: "hey",
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,8 @@ class DiscordClient {
|
||||||
this.guildChannelMap = new Map()
|
this.guildChannelMap = new Map()
|
||||||
if (listen !== "no") {
|
if (listen !== "no") {
|
||||||
this.cloud.on("event", message => {
|
this.cloud.on("event", message => {
|
||||||
process.nextTick(() => {
|
|
||||||
discordPackets.onPacket(this, message, listen)
|
discordPackets.onPacket(this, message, listen)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CONNECTION_DEBUG) {
|
if (CONNECTION_DEBUG) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
const assert = require("assert")
|
const assert = require("assert").strict
|
||||||
const {scheduler} = require("timers/promises")
|
const {scheduler} = require("timers/promises")
|
||||||
const passthrough = require("../passthrough")
|
const passthrough = require("../passthrough")
|
||||||
const {sync} = passthrough
|
const {sync} = passthrough
|
||||||
|
|
@ -8,7 +8,32 @@ const {sync} = passthrough
|
||||||
/** @type {import("../matrix/homeserver-status")} */
|
/** @type {import("../matrix/homeserver-status")} */
|
||||||
const homeserverStatus = sync.require("../matrix/homeserver-status")
|
const homeserverStatus = sync.require("../matrix/homeserver-status")
|
||||||
|
|
||||||
let checkedHomeserver = false
|
const guildReadyStatus = new class {
|
||||||
|
/** @type {Set<string> | null} */
|
||||||
|
unavailableGuilds = null
|
||||||
|
_allReady = Promise.withResolvers()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} guildID
|
||||||
|
* @returns {boolean} true if it was the last one
|
||||||
|
*/
|
||||||
|
makeReady(guildID) {
|
||||||
|
assert(this.unavailableGuilds)
|
||||||
|
if (this.unavailableGuilds.delete(guildID) && this.unavailableGuilds.size === 0) {
|
||||||
|
this._allReady.resolve(null)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
allReady() {
|
||||||
|
return this.unavailableGuilds && this.unavailableGuilds.size === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAllReady() {
|
||||||
|
return this._allReady.promise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import("./discord-client")} client
|
* @param {import("./discord-client")} client
|
||||||
|
|
@ -28,12 +53,27 @@ async function onPacket(client, message, listen) {
|
||||||
client.ready = true
|
client.ready = true
|
||||||
client.user = message.d.user
|
client.user = message.d.user
|
||||||
client.application = message.d.application
|
client.application = message.d.application
|
||||||
|
guildReadyStatus.unavailableGuilds = new Set(message.d.guilds.filter(g => g.unavailable).map(g => g.id))
|
||||||
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})`)
|
||||||
|
process.stdout.write("Waiting for guilds to warm up... ")
|
||||||
interactions.registerInteractions()
|
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 account's member - it's needed for roles to determine private channels on web
|
||||||
client.guilds.set(message.d.id, message.d)
|
client.guilds.set(message.d.id, message.d)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Info about guilds is populated one guild at a time.
|
||||||
|
For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild.
|
||||||
|
If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping.
|
||||||
|
We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver.
|
||||||
|
The "all guilds ready" delay can be removed if ONE of the following is done:
|
||||||
|
1. m->d can queue incoming events until their guild exists in memory
|
||||||
|
2. d->m missed messages can have their errors handled and added to queue, rather than pinging first
|
||||||
|
*/
|
||||||
|
const firstReady = guildReadyStatus.allReady()
|
||||||
|
const lastGuildReady = guildReadyStatus.makeReady(message.d.id)
|
||||||
|
|
||||||
const arr = []
|
const arr = []
|
||||||
client.guildChannelMap.set(message.d.id, arr)
|
client.guildChannelMap.set(message.d.id, arr)
|
||||||
for (const channel of message.d.channels || []) {
|
for (const channel of message.d.channels || []) {
|
||||||
|
|
@ -51,28 +91,14 @@ async function onPacket(client, message, listen) {
|
||||||
|
|
||||||
if (listen === "full") {
|
if (listen === "full") {
|
||||||
try {
|
try {
|
||||||
/*
|
// Wait for guilds to be connected and homeserver to be online. If this is the last guild, a different code path is used to trigger the homeserver check.
|
||||||
Info about guilds is populated one guild at a time.
|
if (lastGuildReady) {
|
||||||
For m->d bridging to work, the guild needs to be populated, so we need to have GUILD_CREATE for the guild.
|
process.stdout.write(`ok, ${client.guilds.size} available.\nConnecting to homeserver... `)
|
||||||
If we ping the homeserver, it will send us any pending events, so we need to wait for all GUILD_CREATES before we ping.
|
// await guildReadyStatus.waitForAllReady() - no need, we already checked this is the last guild
|
||||||
We must attempt a ping because we don't want to try sending missed d->m messages to an offline homeserver.
|
|
||||||
This delay can be removed if ONE of the following is done:
|
|
||||||
1. m->d can queue incoming events until their guild exists in memory
|
|
||||||
2. d->m missed messages can have their errors handled and added to queue, rather than pinging first
|
|
||||||
*/
|
|
||||||
let isMainCharacter = false
|
|
||||||
if (!checkedHomeserver) {
|
|
||||||
checkedHomeserver = true
|
|
||||||
isMainCharacter = true
|
|
||||||
console.log("Warming up guilds~")
|
|
||||||
}
|
|
||||||
await scheduler.wait(5000)
|
|
||||||
if (isMainCharacter) {
|
|
||||||
checkedHomeserver = true
|
|
||||||
process.stdout.write("Connecting to homeserver... ")
|
|
||||||
await homeserverStatus.homeserverStatus.waitForOnline(true)
|
await homeserverStatus.homeserverStatus.waitForOnline(true)
|
||||||
console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.")
|
console.log("ok.\nReplaying past events. Welcome to Out Of Your Element.")
|
||||||
} else {
|
} else {
|
||||||
|
await guildReadyStatus.waitForAllReady()
|
||||||
await homeserverStatus.homeserverStatus.waitForOnline(false)
|
await homeserverStatus.homeserverStatus.waitForOnline(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,7 +107,9 @@ async function onPacket(client, message, listen) {
|
||||||
await eventDispatcher.checkMissedPins(client, message.d)
|
await eventDispatcher.checkMissedPins(client, message.d)
|
||||||
await eventDispatcher.checkMissedLeaves(client, message.d)
|
await eventDispatcher.checkMissedLeaves(client, message.d)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (firstReady) {
|
||||||
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
console.error("Failed to sync missed events. To retry, please fix this error and restart OOYE:")
|
||||||
|
}
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,3 +253,4 @@ async function dispatchPacketToBridge(client, message) {
|
||||||
|
|
||||||
module.exports.onPacket = onPacket
|
module.exports.onPacket = onPacket
|
||||||
module.exports.dispatchPacketToBridge = dispatchPacketToBridge
|
module.exports.dispatchPacketToBridge = dispatchPacketToBridge
|
||||||
|
module.exports.guildReadyStatus = guildReadyStatus
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ async function sendEvent(event) {
|
||||||
channel_id: messageResponse.channel_id,
|
channel_id: messageResponse.channel_id,
|
||||||
guild_id: guild.id,
|
guild_id: guild.id,
|
||||||
embeds: messageResponse.embeds
|
embeds: messageResponse.embeds
|
||||||
}, guild, null)
|
}, guild)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ const setupEmojis = sync.require("../actions/setup-emojis")
|
||||||
const userToMxid = sync.require("../../d2m/converters/user-to-mxid")
|
const userToMxid = sync.require("../../d2m/converters/user-to-mxid")
|
||||||
/** @type {import("../../web/routes/letter-avatar")} */
|
/** @type {import("../../web/routes/letter-avatar")} */
|
||||||
const letterAvatar = sync.require("../../web/routes/letter-avatar")
|
const letterAvatar = sync.require("../../web/routes/letter-avatar")
|
||||||
|
/** @type {import("../../d2m/actions/retrigger")} */
|
||||||
|
const retrigger = sync.require("../../d2m/actions/retrigger")
|
||||||
|
|
||||||
/** @type {[RegExp, string][]} */
|
/** @type {[RegExp, string][]} */
|
||||||
const markdownEscapes = [
|
const markdownEscapes = [
|
||||||
|
|
@ -696,6 +698,7 @@ async function eventToMessage(event, guild, channel, di) {
|
||||||
// Check if we have a pointer to what was edited
|
// Check if we have a pointer to what was edited
|
||||||
const originalEventId = relatesTo.event_id
|
const originalEventId = relatesTo.event_id
|
||||||
if (!originalEventId) return
|
if (!originalEventId) return
|
||||||
|
if (!await retrigger.waitForEvent(originalEventId)) return
|
||||||
messageIDsToEdit = select("event_message", "message_id", {event_id: originalEventId}, "ORDER BY part").pluck().all()
|
messageIDsToEdit = select("event_message", "message_id", {event_id: originalEventId}, "ORDER BY part").pluck().all()
|
||||||
if (!messageIDsToEdit.length) return
|
if (!messageIDsToEdit.length) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,15 @@ const IMAGE_SIZE = 1024
|
||||||
const inflight = new Map()
|
const inflight = new Map()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} url
|
* @param {string} urlString
|
||||||
*/
|
*/
|
||||||
function _removeExpiryParams(url) {
|
function _removeExpiryParams(urlString) {
|
||||||
return url.replace(/\?(?:(?:ex|is|sg|hm)=[a-f0-9]+&?)*$/, "")
|
const url = new URL(urlString)
|
||||||
|
url.searchParams.delete("ex")
|
||||||
|
url.searchParams.delete("is")
|
||||||
|
url.searchParams.delete("sg")
|
||||||
|
url.searchParams.delete("hm")
|
||||||
|
return url.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,9 @@ test("removeExpiryParams: rearranged params are removed", t => {
|
||||||
const result = file._removeExpiryParams(url)
|
const result = file._removeExpiryParams(url)
|
||||||
t.equal(result, "https://cdn.discordapp.com/attachments/112760669178241024/1157363960518029322/image.png")
|
t.equal(result, "https://cdn.discordapp.com/attachments/112760669178241024/1157363960518029322/image.png")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("removeExpiryParams: works on media proxy and keeps quality", t => {
|
||||||
|
const url = "https://media.discordapp.net/attachments/1160894080998461480/1548665030898094240/855064e9-bdae-487a-a497-c662bd510487.png?ex=6aa7e234&is=6aa690b4&hm=7c153efcdd1feb1d36d0bb99955a2bfe12f26abea377fed02980521766f51620&format=jpeg"
|
||||||
|
const result = file._removeExpiryParams(url)
|
||||||
|
t.equal(result, "https://media.discordapp.net/attachments/1160894080998461480/1548665030898094240/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg")
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,8 @@ INSERT INTO file (discord_url, mxc_url) VALUES
|
||||||
('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'),
|
('https://cdn.discordapp.com/attachments/122155380120748034/1174514575220158545/the.yml', 'mxc://cadence.moe/HnQIYQmmlIKwOQsbFsIGpzPP'),
|
||||||
('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'),
|
('https://cdn.discordapp.com/attachments/112760669178241024/1296237494987133070/100km.gif', 'mxc://cadence.moe/qDAotmebTfEIfsAIVCEZptLh'),
|
||||||
('https://cdn.discordapp.com/attachments/123/456/my_enemies.txt', 'mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge'),
|
('https://cdn.discordapp.com/attachments/123/456/my_enemies.txt', 'mxc://cadence.moe/y89EOTRp2lbeOkgdsEleGOge'),
|
||||||
('https://cdn.discordapp.com/emojis/1254940125948022915.webp', 'mxc://cadence.moe/bvVJFgOIyNcAknKCbmaHDktG');
|
('https://cdn.discordapp.com/emojis/1254940125948022915.webp', 'mxc://cadence.moe/bvVJFgOIyNcAknKCbmaHDktG'),
|
||||||
|
('https://media.discordapp.net/attachments/1160894080998461480/1548656385292509274/855064e9-bdae-487a-a497-c662bd510487.png?format=jpeg', 'mxc://cadence.moe/zXNXRvJwRDMIzZZVqHUXtGeD');
|
||||||
|
|
||||||
INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
|
INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
|
||||||
('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'),
|
('230201364309868544', 'hippo', 0, 'mxc://cadence.moe/qWmbXeRspZRLPcjseyLmeyXC'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue