diff --git a/d2m/actions/create-space.js b/d2m/actions/create-space.js index 8628dcb..96b074e 100644 --- a/d2m/actions/create-space.js +++ b/d2m/actions/create-space.js @@ -12,6 +12,8 @@ const api = sync.require("../../matrix/api") const file = sync.require("../../matrix/file") /** @type {import("./create-room")} */ const createRoom = sync.require("./create-room") +/** @type {import("../converters/expression")} */ +const expression = sync.require("../converters/expression") /** @type {import("../../matrix/kstate")} */ const ks = sync.require("../../matrix/kstate") @@ -186,8 +188,29 @@ async function syncSpaceFully(guildID) { 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.ensureSpace = ensureSpace module.exports.syncSpace = syncSpace module.exports.syncSpaceFully = syncSpaceFully module.exports.guildToKState = guildToKState +module.exports.syncSpaceExpressions = syncSpaceExpressions diff --git a/d2m/converters/expression.js b/d2m/converters/expression.js new file mode 100644 index 0000000..146a766 --- /dev/null +++ b/d2m/converters/expression.js @@ -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 diff --git a/d2m/discord-packets.js b/d2m/discord-packets.js index 0476bdd..4d6e9fe 100644 --- a/d2m/discord-packets.js +++ b/d2m/discord-packets.js @@ -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") { client.channels.set(message.d.id, message.d) @@ -98,6 +110,9 @@ const utils = { if (message.t === "GUILD_UPDATE") { 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") { await eventDispatcher.onChannelOrThreadUpdate(client, message.d, false) diff --git a/d2m/event-dispatcher.js b/d2m/event-dispatcher.js index 1301bca..f7ff6a3 100644 --- a/d2m/event-dispatcher.js +++ b/d2m/event-dispatcher.js @@ -37,6 +37,8 @@ module.exports = { console.error(`while handling this ${gatewayMessage.t} gateway event:`) console.dir(gatewayMessage.d, {depth: null}) + if (gatewayMessage.t === "TYPING_START") return + if (Date.now() - lastReportedEvent < 5000) return lastReportedEvent = Date.now() @@ -81,7 +83,7 @@ module.exports = { async checkMissedMessages(client, guild) { if (guild.unavailable) return 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)) { if (!bridgedChannels.includes(channel.id)) continue if (!channel.last_message_id) continue @@ -158,7 +160,7 @@ module.exports = { */ async onMessageCreate(client, message) { 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) { // 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 @@ -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. // (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) + }, + + /** + * @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) } } diff --git a/db/orm.js b/db/orm.js index 424ede5..6195753 100644 --- a/db/orm.js +++ b/db/orm.js @@ -32,6 +32,7 @@ class From { this.sql = "" this.cols = [] this.using = [] + this.isPluck = false } /** @@ -69,6 +70,7 @@ class From { const r = this r.constructor = Pluck r.cols = [col] + r.isPluck = true return r } @@ -89,7 +91,8 @@ class From { } sql += this.sql /** @type {U.Prepared, Col>>} */ - const prepared = db.prepare(sql) + let prepared = db.prepare(sql) + if (this.isPluck) prepared = prepared.pluck() return prepared } diff --git a/db/orm.test.js b/db/orm.test.js new file mode 100644 index 0000000..6043e77 --- /dev/null +++ b/db/orm.test.js @@ -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") +}) diff --git a/test/test.js b/test/test.js index f158ddc..c2ec57b 100644 --- a/test/test.js +++ b/test/test.js @@ -25,6 +25,7 @@ passthrough.select = orm.select const file = sync.require("../matrix/file") 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/api.test") require("../matrix/read-registration.test")