Compare commits

...

3 commits

7 changed files with 168 additions and 3 deletions

View file

@ -12,6 +12,8 @@ const api = sync.require("../../matrix/api")
const file = sync.require("../../matrix/file") const file = sync.require("../../matrix/file")
/** @type {import("./create-room")} */ /** @type {import("./create-room")} */
const createRoom = sync.require("./create-room") const createRoom = sync.require("./create-room")
/** @type {import("../converters/expression")} */
const expression = sync.require("../converters/expression")
/** @type {import("../../matrix/kstate")} */ /** @type {import("../../matrix/kstate")} */
const ks = sync.require("../../matrix/kstate") const ks = sync.require("../../matrix/kstate")
@ -186,8 +188,29 @@ async function syncSpaceFully(guildID) {
return spaceID return spaceID
} }
/**
* @param {import("discord-api-types/v10").GatewayGuildEmojisUpdateDispatchData | import("discord-api-types/v10").GatewayGuildStickersUpdateDispatchData} data
*/
async function syncSpaceExpressions(data) {
// No need for kstate here. Each of these maps to a single state event, which will always overwrite what was there before. I can just send the state event.
const spaceID = select("guild_space", "space_id", "WHERE guild_id = ?").pluck().get(data.guild_id)
if (!spaceID) return
if ("emojis" in data && data.emojis.length) {
const content = await expression.emojisToState(data.emojis)
api.sendState(spaceID, "im.ponies.room_emotes", "moe.cadence.ooye.pack.emojis", content)
}
if ("stickers" in data && data.stickers.length) {
const content = await expression.stickersToState(data.stickers)
api.sendState(spaceID, "im.ponies.room_emotes", "moe.cadence.ooye.pack.stickers", content)
}
}
module.exports.createSpace = createSpace module.exports.createSpace = createSpace
module.exports.ensureSpace = ensureSpace module.exports.ensureSpace = ensureSpace
module.exports.syncSpace = syncSpace module.exports.syncSpace = syncSpace
module.exports.syncSpaceFully = syncSpaceFully module.exports.syncSpaceFully = syncSpaceFully
module.exports.guildToKState = guildToKState module.exports.guildToKState = guildToKState
module.exports.syncSpaceExpressions = syncSpaceExpressions

View file

@ -0,0 +1,82 @@
// @ts-check
const assert = require("assert").strict
const DiscordTypes = require("discord-api-types/v10")
const passthrough = require("../../passthrough")
const {discord, sync, db, select} = passthrough
/** @type {import("../../matrix/file")} */
const file = sync.require("../../matrix/file")
/**
* @param {DiscordTypes.APIEmoji[]} emojis
*/
async function emojisToState(emojis) {
const result = {
pack: {
display_name: "Discord Emojis",
usage: ["emoticon"] // we'll see...
},
images: {
}
}
await Promise.all(emojis.map(emoji =>
// the homeserver can probably cope with doing this in parallel
file.uploadDiscordFileToMxc(file.emoji(emoji.id, emoji.animated)).then(url => {
result.images[emoji.name] = {
info: {
mimetype: emoji.animated ? "image/gif" : "image/png"
},
url
}
}).catch(e => {
if (e.data.errcode === "M_TOO_LARGE") { // Lol.
return
}
console.error(`Trying to handle emoji ${emoji.name} (${emoji.id}), but...`)
throw e
})
))
return result
}
/**
* @param {DiscordTypes.APISticker[]} stickers
*/
async function stickersToState(stickers) {
const result = {
pack: {
display_name: "Discord Stickers",
usage: ["sticker"] // we'll see...
},
images: {
}
}
const shortcodes = []
await Promise.all(stickers.map(sticker =>
// the homeserver can probably cope with doing this in parallel
file.uploadDiscordFileToMxc(file.sticker(sticker)).then(url => {
/** @type {string | undefined} */
let body = sticker.name
if (sticker && sticker.description) body += ` - ${sticker.description}`
if (!body) body = undefined
let shortcode = sticker.name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, "-").replace(/^-|-$/g, "").replace(/--+/g, "-")
while (shortcodes.includes(shortcode)) shortcode = shortcode + "~"
shortcodes.push(shortcode)
result.images[shortcodes] = {
info: {
mimetype: file.stickerFormat.get(sticker.format_type)?.mime || "image/png"
},
body,
url
}
})
))
return result
}
module.exports.emojisToState = emojisToState
module.exports.stickersToState = stickersToState

View file

@ -56,6 +56,18 @@ const utils = {
} }
} }
} else if (message.t === "GUILD_EMOJIS_UPDATE") {
const guild = client.guilds.get(message.d.guild_id)
if (guild) {
guild.emojis = message.d.emojis
}
} else if (message.t === "GUILD_STICKERS_UPDATE") {
const guild = client.guilds.get(message.d.guild_id)
if (guild) {
guild.stickers = message.d.stickers
}
} else if (message.t === "THREAD_CREATE") { } else if (message.t === "THREAD_CREATE") {
client.channels.set(message.d.id, message.d) client.channels.set(message.d.id, message.d)
@ -98,6 +110,9 @@ const utils = {
if (message.t === "GUILD_UPDATE") { if (message.t === "GUILD_UPDATE") {
await eventDispatcher.onGuildUpdate(client, message.d) await eventDispatcher.onGuildUpdate(client, message.d)
} else if (message.t === "GUILD_EMOJIS_UPDATE" || message.t === "GUILD_STICKERS_UPDATE") {
await eventDispatcher.onExpressionsUpdate(client, message.d)
} else if (message.t === "CHANNEL_UPDATE") { } else if (message.t === "CHANNEL_UPDATE") {
await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false)

View file

@ -37,6 +37,8 @@ module.exports = {
console.error(`while handling this ${gatewayMessage.t} gateway event:`) console.error(`while handling this ${gatewayMessage.t} gateway event:`)
console.dir(gatewayMessage.d, {depth: null}) console.dir(gatewayMessage.d, {depth: null})
if (gatewayMessage.t === "TYPING_START") return
if (Date.now() - lastReportedEvent < 5000) return if (Date.now() - lastReportedEvent < 5000) return
lastReportedEvent = Date.now() lastReportedEvent = Date.now()
@ -81,7 +83,7 @@ module.exports = {
async checkMissedMessages(client, guild) { async checkMissedMessages(client, guild) {
if (guild.unavailable) return if (guild.unavailable) return
const bridgedChannels = select("channel_room", "channel_id").pluck().all() const bridgedChannels = select("channel_room", "channel_id").pluck().all()
const prepared = select("event_message", "1", "WHERE message_id = ?").pluck() const prepared = select("event_message", "event_id", "WHERE message_id = ?").pluck()
for (const channel of guild.channels.concat(guild.threads)) { for (const channel of guild.channels.concat(guild.threads)) {
if (!bridgedChannels.includes(channel.id)) continue if (!bridgedChannels.includes(channel.id)) continue
if (!channel.last_message_id) continue if (!channel.last_message_id) continue
@ -158,7 +160,7 @@ module.exports = {
*/ */
async onMessageCreate(client, message) { async onMessageCreate(client, message) {
if (message.webhook_id) { if (message.webhook_id) {
const row = select("webhook", "1", "WHERE webhook_id = ?").pluck().get(message.webhook_id) const row = select("webhook", "webhook_id", "WHERE webhook_id = ?").pluck().get(message.webhook_id)
if (row) { if (row) {
// The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it. // The message was sent by the bridge's own webhook on discord. We don't want to reflect this back, so just drop it.
return return
@ -230,5 +232,13 @@ module.exports = {
// Discord does not send typing stopped events, so typing only stops if the timeout is reached or if the user sends their message. // Discord does not send typing stopped events, so typing only stops if the timeout is reached or if the user sends their message.
// (We have to manually stop typing on Matrix-side when the message is sent. This is part of the send action.) // (We have to manually stop typing on Matrix-side when the message is sent. This is part of the send action.)
await api.sendTyping(roomID, true, mxid, 10000) await api.sendTyping(roomID, true, mxid, 10000)
},
/**
* @param {import("./discord-client")} client
* @param {import("discord-api-types/v10").GatewayGuildEmojisUpdateDispatchData | import("discord-api-types/v10").GatewayGuildStickersUpdateDispatchData} data
*/
async onExpressionsUpdate(client, data) {
await createSpace.syncSpaceExpressions(data)
} }
} }

View file

@ -32,6 +32,7 @@ class From {
this.sql = "" this.sql = ""
this.cols = [] this.cols = []
this.using = [] this.using = []
this.isPluck = false
} }
/** /**
@ -69,6 +70,7 @@ class From {
const r = this const r = this
r.constructor = Pluck r.constructor = Pluck
r.cols = [col] r.cols = [col]
r.isPluck = true
return r return r
} }
@ -89,7 +91,8 @@ class From {
} }
sql += this.sql sql += this.sql
/** @type {U.Prepared<Pick<U.Merge<U.Models[Table]>, Col>>} */ /** @type {U.Prepared<Pick<U.Merge<U.Models[Table]>, Col>>} */
const prepared = db.prepare(sql) let prepared = db.prepare(sql)
if (this.isPluck) prepared = prepared.pluck()
return prepared return prepared
} }

31
db/orm.test.js Normal file
View file

@ -0,0 +1,31 @@
// @ts-check
const {test} = require("supertape")
const data = require("../test/data")
const {db, select, from} = require("../passthrough")
test("orm: select: get works", t => {
const row = select("guild_space", "guild_id", "WHERE space_id = ?").get("!jjWAGMeQdNrVZSSfvz:cadence.moe")
t.equal(row?.guild_id, data.guild.general.id)
})
test("orm: from: get works", t => {
const row = from("guild_space").select("guild_id").and("WHERE space_id = ?").get("!jjWAGMeQdNrVZSSfvz:cadence.moe")
t.equal(row?.guild_id, data.guild.general.id)
})
test("orm: select: get pluck works", t => {
const guildID = select("guild_space", "guild_id", "WHERE space_id = ?").pluck().get("!jjWAGMeQdNrVZSSfvz:cadence.moe")
t.equal(guildID, data.guild.general.id)
})
test("orm: from: get pluck works", t => {
const guildID = from("guild_space").pluck("guild_id").and("WHERE space_id = ?").get("!jjWAGMeQdNrVZSSfvz:cadence.moe")
t.equal(guildID, data.guild.general.id)
})
test("orm: from: join and pluck works", t => {
const mxid = from("sim").join("sim_member", "mxid").and("WHERE discord_id = ? AND room_id = ?").pluck("mxid").get("771520384671416320", "!uCtjHhfGlYbVnPVlkG:cadence.moe")
t.equal(mxid, "@_ooye_bojack_horseman:cadence.moe")
})

View file

@ -25,6 +25,7 @@ passthrough.select = orm.select
const file = sync.require("../matrix/file") const file = sync.require("../matrix/file")
file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) } file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not allowed to upload files during testing.\nURL: ${url}`) }
require("../db/orm.test")
require("../matrix/kstate.test") require("../matrix/kstate.test")
require("../matrix/api.test") require("../matrix/api.test")
require("../matrix/read-registration.test") require("../matrix/read-registration.test")