Compare commits

..

10 commits

23 changed files with 205 additions and 100 deletions

View file

@ -120,8 +120,7 @@ async function channelToKState(channel, guild) {
if (customAvatar) { if (customAvatar) {
avatarEventContent.url = customAvatar avatarEventContent.url = customAvatar
} else if (guild.icon) { } else if (guild.icon) {
avatarEventContent.discord_path = file.guildIcon(guild) avatarEventContent.url = {$url: file.guildIcon(guild)}
avatarEventContent.url = await file.uploadDiscordFileToMxc(avatarEventContent.discord_path) // TODO: somehow represent future values in kstate (callbacks?), while still allowing for diffing, so test cases don't need to touch the media API
} }
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel] let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]

View file

@ -65,7 +65,6 @@ async function guildToKState(guild, privacyLevel) {
"m.room.name/": {name: guild.name}, "m.room.name/": {name: guild.name},
"m.room.avatar/": { "m.room.avatar/": {
$if: guild.icon, $if: guild.icon,
discord_path: file.guildIcon(guild),
url: {$url: file.guildIcon(guild)} url: {$url: file.guildIcon(guild)}
}, },
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]}, "m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},

View file

@ -14,7 +14,6 @@ test("guild2space: can generate kstate for a guild, passing privacy level 0", as
await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))), await kstateUploadMxc(kstateStripConditionals(await guildToKState(testData.guild.general, 0))),
{ {
"m.room.avatar/": { "m.room.avatar/": {
discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF" url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
}, },
"m.room.guest_access/": { "m.room.guest_access/": {

View file

@ -6,6 +6,13 @@ const Ty = require("../../types")
test("message2event embeds: nothing but a field", async t => { test("message2event embeds: nothing but a field", async t => {
const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {}) const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {})
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message",
body: "> ↪️ @papiophidian: used `/stats`",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">@papiophidian</a> used <code>/stats</code></blockquote>",
"m.mentions": {},
msgtype: "m.text",
}, {
$type: "m.room.message", $type: "m.room.message",
"m.mentions": {}, "m.mentions": {},
msgtype: "m.notice", msgtype: "m.notice",
@ -143,6 +150,13 @@ test("message2event embeds: crazy html is all escaped", async t => {
test("message2event embeds: title without url", async t => { test("message2event embeds: title without url", async t => {
const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general) const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general)
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message",
body: "> ↪️ @papiophidian: used `/stats`",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">@papiophidian</a> used <code>/stats</code></blockquote>",
"m.mentions": {},
msgtype: "m.text",
}, {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!", body: "| ## Hi, I'm Amanda!\n| \n| I condone pirating music!",
@ -155,6 +169,13 @@ test("message2event embeds: title without url", async t => {
test("message2event embeds: url without title", async t => { test("message2event embeds: url without title", async t => {
const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general) const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general)
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message",
body: "> ↪️ @papiophidian: used `/stats`",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">@papiophidian</a> used <code>/stats</code></blockquote>",
"m.mentions": {},
msgtype: "m.text",
}, {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: "| I condone pirating music!", body: "| I condone pirating music!",
@ -167,6 +188,13 @@ test("message2event embeds: url without title", async t => {
test("message2event embeds: author without url", async t => { test("message2event embeds: author without url", async t => {
const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general) const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general)
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message",
body: "> ↪️ @papiophidian: used `/stats`",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">@papiophidian</a> used <code>/stats</code></blockquote>",
"m.mentions": {},
msgtype: "m.text",
}, {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: "| ## Amanda\n| \n| I condone pirating music!", body: "| ## Amanda\n| \n| I condone pirating music!",
@ -179,6 +207,13 @@ test("message2event embeds: author without url", async t => {
test("message2event embeds: author url without name", async t => { test("message2event embeds: author url without name", async t => {
const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general) const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general)
t.deepEqual(events, [{ t.deepEqual(events, [{
$type: "m.room.message",
body: "> ↪️ @papiophidian: used `/stats`",
format: "org.matrix.custom.html",
formatted_body: "<blockquote>↪️ <a href=\"https://matrix.to/#/@_ooye_papiophidian:cadence.moe\">@papiophidian</a> used <code>/stats</code></blockquote>",
"m.mentions": {},
msgtype: "m.text",
}, {
$type: "m.room.message", $type: "m.room.message",
msgtype: "m.notice", msgtype: "m.notice",
body: "| I condone pirating music!", body: "| I condone pirating music!",

View file

@ -32,7 +32,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
/** @param {{id: string, type: "discordUser"}} node */ /** @param {{id: string, type: "discordUser"}} node */
user: node => { user: node => {
const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get() const mxid = select("sim", "mxid", {user_id: node.id}).pluck().get()
const username = message.mentions.find(ment => ment.id === node.id)?.username || node.id const interaction = message.interaction_metadata || message.interaction
const username = message.mentions.find(ment => ment.id === node.id)?.username
|| (interaction?.user.id === node.id ? interaction.user.username : null)
|| node.id
if (mxid && useHTML) { if (mxid && useHTML) {
return `<a href="https://matrix.to/#/${mxid}">@${username}</a>` return `<a href="https://matrix.to/#/${mxid}">@${username}</a>`
} else { } else {
@ -229,6 +232,13 @@ async function messageToEvent(message, guild, options = {}, di) {
}] }]
} }
const interaction = message.interaction_metadata || message.interaction
if (message.type === DiscordTypes.MessageType.ChatInputCommand && interaction && "name" in interaction) {
// Commands are sent by the responding bot. Need to attach the metadata of the person using the command at the top.
if (message.content) message.content = `\n${message.content}`
message.content = `> ↪️ <@${interaction.user.id}> used \`/${interaction.name}\`${message.content}`
}
/** /**
@type {{room?: boolean, user_ids?: string[]}} @type {{room?: boolean, user_ids?: string[]}}
We should consider the following scenarios for mentions: We should consider the following scenarios for mentions:
@ -363,7 +373,7 @@ async function messageToEvent(message, guild, options = {}, di) {
// Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter. // Handling emojis that we don't know about. The emoji has to be present in the DB for it to be picked up in the emoji markdown converter.
// So we scan the message ahead of time for all its emojis and ensure they are in the DB. // So we scan the message ahead of time for all its emojis and ensure they are in the DB.
const emojiMatches = [...content.matchAll(/<(a?):([^:>]{2,64}):([0-9]+)>/g)] const emojiMatches = [...content.matchAll(/<(a?):([^:>]{1,64}):([0-9]+)>/g)]
await Promise.all(emojiMatches.map(match => { await Promise.all(emojiMatches.map(match => {
const id = match[3] const id = match[3]
const name = match[2] const name = match[2]

View file

@ -57,6 +57,9 @@ class DiscordClient {
addEventLogger("error", "Error") addEventLogger("error", "Error")
addEventLogger("disconnected", "Disconnected") addEventLogger("disconnected", "Disconnected")
addEventLogger("ready", "Ready") addEventLogger("ready", "Ready")
this.snow.requestHandler.on("requestError", (requestID, error) => {
console.error("request error:", error)
})
} }
} }

View file

@ -181,7 +181,7 @@ const utils = {
} catch (e) { } catch (e) {
// Let OOYE try to handle errors too // Let OOYE try to handle errors too
eventDispatcher.onError(client, e, message) await eventDispatcher.onError(client, e, message)
} }
} }
} }

View file

@ -50,7 +50,7 @@ module.exports = {
* @param {Error} e * @param {Error} e
* @param {import("cloudstorm").IGatewayMessage} gatewayMessage * @param {import("cloudstorm").IGatewayMessage} gatewayMessage
*/ */
onError(client, e, gatewayMessage) { async onError(client, e, gatewayMessage) {
console.error("hit event-dispatcher's error handler with this exception:") console.error("hit event-dispatcher's error handler with this exception:")
console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later console.error(e) // TODO: also log errors into a file or into the database, maybe use a library for this? or just wing it? definitely need to be able to store the formatted event body to load back in later
console.error(`while handling this ${gatewayMessage.t} gateway event:`) console.error(`while handling this ${gatewayMessage.t} gateway event:`)
@ -83,7 +83,7 @@ module.exports = {
builder.addLine(`Error trace:\n${stackLines.join("\n")}`, `<details><summary>Error trace</summary><pre>${stackLines.join("\n")}</pre></details>`) builder.addLine(`Error trace:\n${stackLines.join("\n")}`, `<details><summary>Error trace</summary><pre>${stackLines.join("\n")}</pre></details>`)
} }
builder.addLine("", `<details><summary>Original payload</summary><pre>${util.inspect(gatewayMessage.d, false, 4, false)}</pre></details>`) builder.addLine("", `<details><summary>Original payload</summary><pre>${util.inspect(gatewayMessage.d, false, 4, false)}</pre></details>`)
api.sendEvent(roomID, "m.room.message", { await api.sendEvent(roomID, "m.room.message", {
...builder.get(), ...builder.get(),
"moe.cadence.ooye.error": { "moe.cadence.ooye.error": {
source: "discord", source: "discord",

1
db/orm-defs.d.ts vendored
View file

@ -114,3 +114,4 @@ export type AllKeys<U> = U extends any ? keyof U : never
export type PickTypeOf<T, K extends AllKeys<T>> = T extends { [k in K]?: any } ? T[K] : never export type PickTypeOf<T, K extends AllKeys<T>> = T extends { [k in K]?: any } ? T[K] : never
export type Merge<U> = {[x in AllKeys<U>]: PickTypeOf<U, x>} export type Merge<U> = {[x in AllKeys<U>]: PickTypeOf<U, x>}
export type Nullable<T> = {[k in keyof T]: T[k] | null} export type Nullable<T> = {[k in keyof T]: T[k] | null}
export type Numberish<T> = {[k in keyof T]: T[k] extends number ? (number | bigint) : T[k]}

View file

@ -8,7 +8,7 @@ const U = require("./orm-defs")
* @template {keyof U.Models[Table]} Col * @template {keyof U.Models[Table]} Col
* @param {Table} table * @param {Table} table
* @param {Col[] | Col} cols * @param {Col[] | Col} cols
* @param {Partial<U.Models[Table]>} where * @param {Partial<U.Numberish<U.Models[Table]>>} where
* @param {string} [e] * @param {string} [e]
*/ */
function select(table, cols, where = {}, e = "") { function select(table, cols, where = {}, e = "") {
@ -108,7 +108,7 @@ class From {
} }
/** /**
* @param {Partial<U.Models[Table]>} conditions * @param {Partial<U.Numberish<U.Models[Table]>>} conditions
*/ */
where(conditions) { where(conditions) {
const wheres = Object.entries(conditions).map(([col, value]) => { const wheres = Object.entries(conditions).map(([col, value]) => {

View file

@ -39,7 +39,7 @@ async function getCachedHierarchy(spaceID) {
/** @type {{name: string, value: string}[]} */ /** @type {{name: string, value: string}[]} */
const childRooms = [] const childRooms = []
for (const room of result) { for (const room of result) {
if (room.name) { if (room.name && !room.name.match(/^\[[⛓️🔊]\]/) && room.room_type !== "m.space") {
childRooms.push({name: room.name, value: room.room_id}) childRooms.push({name: room.name, value: room.room_id})
reverseCache.set(room.room_id, spaceID) reverseCache.set(room.room_id, spaceID)
} }

View file

@ -7,31 +7,34 @@ const {discord, sync, db, select, from} = require("../../passthrough")
/** @type {import("../../matrix/api")} */ /** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api") const api = sync.require("../../matrix/api")
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */ /**
async function interact({id, token, data, channel, member, guild_id}) { * @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
*/
async function _interact({data, channel, guild_id}) {
// Check guild is bridged // Check guild is bridged
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
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 (!spaceID || !roomID) return discord.snow.interaction.createInteractionResponse(id, token, { if (!spaceID || !roomID) return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: "This server isn't bridged to Matrix, so you can't invite Matrix users.", content: "This server isn't bridged to Matrix, so you can't invite Matrix users.",
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
// Get named MXID // Get named MXID
/** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore /** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore
const options = data.options const options = data.options
const input = options?.[0].value || "" const input = options?.[0].value || ""
const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0] const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0]
if (!mxid) return discord.snow.interaction.createInteractionResponse(id, token, { if (!mxid) return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`", content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`",
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
// Check for existing invite to the space // Check for existing invite to the space
let spaceMember let spaceMember
@ -39,24 +42,24 @@ async function interact({id, token, data, channel, member, guild_id}) {
spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid) spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid)
} catch (e) {} } catch (e) {}
if (spaceMember && spaceMember.membership === "invite") { if (spaceMember && spaceMember.membership === "invite") {
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`, content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
} }
// Invite Matrix user if not in space // Invite Matrix user if not in space
if (!spaceMember || spaceMember.membership !== "join") { if (!spaceMember || spaceMember.membership !== "join") {
await api.inviteToRoom(spaceID, mxid) await api.inviteToRoom(spaceID, mxid)
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `You invited \`${mxid}\` to the server.` content: `You invited \`${mxid}\` to the server.`
} }
}) }
} }
// The Matrix user *is* in the space, maybe we want to invite them to this channel? // The Matrix user *is* in the space, maybe we want to invite them to this channel?
@ -65,7 +68,7 @@ async function interact({id, token, data, channel, member, guild_id}) {
roomMember = await api.getStateEvent(roomID, "m.room.member", mxid) roomMember = await api.getStateEvent(roomID, "m.room.member", mxid)
} catch (e) {} } catch (e) {}
if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) { if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) {
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`, content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`,
@ -80,34 +83,49 @@ async function interact({id, token, data, channel, member, guild_id}) {
}] }]
}] }]
} }
}) }
} }
// The Matrix user *is* in the space and in the channel. // The Matrix user *is* in the space and in the channel.
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `\`${mxid}\` is already in this server and this channel.`, content: `\`${mxid}\` is already in this server and this channel.`,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
} }
/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */ /**
async function interactButton({id, token, data, channel, member, guild_id, message}) { * @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
*/
async function _interactButton({channel, message}) {
const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1]
assert(mxid) assert(mxid)
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()
await api.inviteToRoom(roomID, mxid) await api.inviteToRoom(roomID, mxid)
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.UpdateMessage, type: DiscordTypes.InteractionResponseType.UpdateMessage,
data: { data: {
content: `You invited \`${mxid}\` to the channel.`, content: `You invited \`${mxid}\` to the channel.`,
flags: DiscordTypes.MessageFlags.Ephemeral, flags: DiscordTypes.MessageFlags.Ephemeral,
components: [] components: []
} }
}) }
}
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */
async function interact(interaction) {
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
}
/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */
async function interactButton(interaction) {
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction))
} }
module.exports.interact = interact module.exports.interact = interact
module.exports.interactButton = interactButton module.exports.interactButton = interactButton
module.exports._interact = _interact
module.exports._interactButton = _interactButton

View file

@ -7,7 +7,8 @@ const {discord, sync, db, select, from} = require("../../passthrough")
const api = sync.require("../../matrix/api") const api = sync.require("../../matrix/api")
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ /** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
async function interact({id, token, data}) { /** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */
async function interact({id, token, guild_id, channel, data}) {
const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id") const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id")
.select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get() .select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get()
@ -21,12 +22,15 @@ async function interact({id, token, data}) {
}) })
} }
const idInfo = `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``
if (message.source === 1) { // from Discord if (message.source === 1) { // from Discord
const userID = data.resolved.messages[data.target_id].author.id
return discord.snow.interaction.createInteractionResponse(id, token, { return discord.snow.interaction.createInteractionResponse(id, token, {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `This message was bridged to [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix.` content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord to [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix.`
+ `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``, + idInfo,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) })
@ -37,9 +41,8 @@ async function interact({id, token, data}) {
return discord.snow.interaction.createInteractionResponse(id, token, { return discord.snow.interaction.createInteractionResponse(id, token, {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `This message was bridged from [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix.` content: `Bridged [${event.sender}](<https://matrix.to/#/${event.sender}>)'s message in [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix to https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord.`
+ `\nIt was originally sent by [${event.sender}](<https://matrix.to/#/${event.sender}>).` + idInfo,
+ `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) })

View file

@ -5,23 +5,27 @@ const Ty = require("../../types")
const {discord, sync, db, select, from} = require("../../passthrough") const {discord, sync, db, select, from} = require("../../passthrough")
const assert = require("assert/strict") const assert = require("assert/strict")
/** @type {import("../../matrix/api")} */ /** @type {import("../../matrix/api")} */
const api = sync.require("../../matrix/api") const api = sync.require("../../matrix/api")
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */ /**
async function interact({data, channel, id, token, guild_id}) { * @param {DiscordTypes.APIContextMenuGuildInteraction} interaction
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
*/
async function _interact({data, channel, guild_id}) {
const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get() const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get()
assert(row) assert(row)
// Can't operate on Discord users // Can't operate on Discord users
if (row.source === 1) { // discord if (row.source === 1) { // discord
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `This command is only meaningful for Matrix users.`, content: `This command is only meaningful for Matrix users.`,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
} }
// Get the message sender, the person that will be inspected/edited // Get the message sender, the person that will be inspected/edited
@ -42,16 +46,16 @@ async function interact({data, channel, id, token, guild_id}) {
// Administrators equal to the bot cannot be demoted // Administrators equal to the bot cannot be demoted
if (userPower >= 100) { if (userPower >= 100) {
return discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `\`${sender}\` has administrator permissions. This cannot be edited.`, content: `\`${sender}\` has administrator permissions. This cannot be edited.`,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
} }
}) }
} }
await discord.snow.interaction.createInteractionResponse(id, token, { return {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
data: { data: {
content: `Showing permissions for \`${sender}\`. Click to edit.`, content: `Showing permissions for \`${sender}\`. Click to edit.`,
@ -79,30 +83,47 @@ async function interact({data, channel, id, token, guild_id}) {
} }
] ]
} }
}) }
} }
/** @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction */ /**
async function interactEdit({data, channel, id, token, guild_id, message}) { * @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction
*/
async function interactEdit({data, id, token, guild_id, message}) {
// Get the person that will be inspected/edited // Get the person that will be inspected/edited
const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1] const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1]
assert(mxid) assert(mxid)
const permission = data.values[0]
const power = permission === "moderator" ? 50 : 0
await discord.snow.interaction.createInteractionResponse(id, token, {
type: DiscordTypes.InteractionResponseType.UpdateMessage,
data: {
content: `Updating \`${mxid}\` to **${permission}**, please wait...`,
components: []
}
})
// Get the space, where the power levels will be inspected/edited // Get the space, where the power levels will be inspected/edited
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get() const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
assert(spaceID) assert(spaceID)
// Do it // Do it
const permission = data.values[0] await api.setUserPowerCascade(spaceID, mxid, power)
const power = permission === "moderator" ? 50 : 0
await api.setUserPower(spaceID, mxid, power)
// TODO: Cascade permissions through room hierarchy (make a helper for this already, geez...)
// ACK // ACK
await discord.snow.interaction.createInteractionResponse(id, token, { await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, {
type: DiscordTypes.InteractionResponseType.DeferredMessageUpdate content: `Updated \`${mxid}\` to **${permission}**.`,
components: []
}) })
} }
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
async function interact(interaction) {
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
}
module.exports.interact = interact module.exports.interact = interact
module.exports.interactEdit = interactEdit module.exports.interactEdit = interactEdit
module.exports._interact = _interact

View file

@ -48,23 +48,23 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
}]) }])
async function dispatchInteraction(interaction) { async function dispatchInteraction(interaction) {
const id = interaction.data.custom_id || interaction.data.name const interactionId = interaction.data.custom_id || interaction.data.name
try { try {
console.log(interaction) console.log(interaction)
if (id === "Matrix info") { if (interactionId === "Matrix info") {
await matrixInfo.interact(interaction) await matrixInfo.interact(interaction)
} else if (id === "invite") { } else if (interactionId === "invite") {
await invite.interact(interaction) await invite.interact(interaction)
} else if (id === "invite_channel") { } else if (interactionId === "invite_channel") {
await invite.interactButton(interaction) await invite.interactButton(interaction)
} else if (id === "Permissions") { } else if (interactionId === "Permissions") {
await permissions.interact(interaction) await permissions.interact(interaction)
} else if (id === "permissions_edit") { } else if (interactionId === "permissions_edit") {
await permissions.interactEdit(interaction) await permissions.interactEdit(interaction)
} else if (id === "bridge") { } else if (interactionId === "bridge") {
await bridge.interact(interaction) await bridge.interact(interaction)
} else { } else {
throw new Error(`Unknown interaction ${id}`) throw new Error(`Unknown interaction ${interactionId}`)
} }
} catch (e) { } catch (e) {
let stackLines = null let stackLines = null
@ -75,14 +75,11 @@ async function dispatchInteraction(interaction) {
stackLines = stackLines.slice(0, cloudstormLine - 2) stackLines = stackLines.slice(0, cloudstormLine - 2)
} }
} }
discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, { await discord.snow.interaction.createFollowupMessage(id, interaction.token, {
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource, content: `Interaction failed: **${interactionId}**`
data: {
content: `Interaction failed: **${id}**`
+ `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\`` + `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\``
+ `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``, + `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``,
flags: DiscordTypes.MessageFlags.Ephemeral flags: DiscordTypes.MessageFlags.Ephemeral
}
}) })
} }
} }

View file

@ -97,8 +97,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
* @param {DiscordTypes.APIMessage} message * @param {DiscordTypes.APIMessage} message
*/ */
function isWebhookMessage(message) { function isWebhookMessage(message) {
const isInteractionResponse = message.type === 20 return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
return message.webhook_id && !isInteractionResponse
} }
/** /**

View file

@ -25,7 +25,6 @@ async function deleteMessage(event) {
*/ */
async function removeReaction(event) { async function removeReaction(event) {
const hash = utils.getEventIDHash(event.redacts) const hash = utils.getEventIDHash(event.redacts)
// TODO: this works but fix the type
const row = from("reaction").join("message_channel", "message_id").select("channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get() const row = from("reaction").join("message_channel", "message_id").select("channel_id", "message_id", "encoded_emoji").where({hashed_event_id: hash}).get()
if (!row) return if (!row) return
await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji) await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji)

View file

@ -260,6 +260,21 @@ async function setUserPower(roomID, mxid, power) {
return powerLevels return powerLevels
} }
/**
* Set a user's power level for a whole room hierarchy.
* @param {string} roomID
* @param {string} mxid
* @param {number} power
*/
async function setUserPowerCascade(roomID, mxid, power) {
assert(roomID[0] === "!")
assert(mxid[0] === "@")
const rooms = await getFullHierarchy(roomID)
for (const room of rooms) {
await setUserPower(room.room_id, mxid, power)
}
}
module.exports.path = path module.exports.path = path
module.exports.register = register module.exports.register = register
module.exports.createRoom = createRoom module.exports.createRoom = createRoom
@ -281,3 +296,4 @@ module.exports.sendTyping = sendTyping
module.exports.profileSetDisplayname = profileSetDisplayname module.exports.profileSetDisplayname = profileSetDisplayname
module.exports.profileSetAvatarUrl = profileSetAvatarUrl module.exports.profileSetAvatarUrl = profileSetAvatarUrl
module.exports.setUserPower = setUserPower module.exports.setUserPower = setUserPower
module.exports.setUserPowerCascade = setUserPowerCascade

View file

@ -1,7 +1,6 @@
// @ts-check // @ts-check
const {db, from} = require("../passthrough") const {db, from} = require("../passthrough")
const api = require("./api")
const reg = require("./read-registration") const reg = require("./read-registration")
const ks = require("./kstate") const ks = require("./kstate")
const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room") const {applyKStateDiffToRoom, roomToKState} = require("../d2m/actions/create-room")
@ -11,13 +10,16 @@ for (const mxid of reg.ooye.invite) {
db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*") db.prepare("INSERT OR IGNORE INTO member_power (mxid, room_id, power_level) VALUES (?, ?, 100)").run(mxid, "*")
} }
// Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. /** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */
const rows = from("member_cache").join("member_power", "mxid") function _getAffectedRooms() {
return from("member_cache").join("member_power", "mxid")
.and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level") .and("where member_power.room_id = '*' and member_cache.power_level != member_power.power_level")
.selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level") .selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level")
.all() .all()
}
;(async () => { async function applyPower() {
const rows = _getAffectedRooms()
for (const row of rows) { for (const row of rows) {
const kstate = await roomToKState(row.room_id) const kstate = await roomToKState(row.room_id)
const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}}) const diff = ks.diffKState(kstate, {"m.room.power_levels/": {users: {[row.mxid]: row.power_level}}})
@ -26,4 +28,7 @@ const rows = from("member_cache").join("member_power", "mxid")
// but we update it here anyway since the homeserver does not always deliver the event round-trip. // but we update it here anyway since the homeserver does not always deliver the event round-trip.
db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid) db.prepare("UPDATE member_cache SET power_level = ? WHERE room_id = ? AND mxid = ?").run(row.power_level, row.room_id, row.mxid)
} }
})() }
module.exports._getAffectedRooms = _getAffectedRooms
module.exports.applyPower = applyPower

View file

@ -25,17 +25,14 @@ const orm = sync.require("./db/orm")
passthrough.from = orm.from passthrough.from = orm.from
passthrough.select = orm.select passthrough.select = orm.select
const power = require("./matrix/power.js")
sync.require("./m2d/event-dispatcher") sync.require("./m2d/event-dispatcher")
discord.snow.requestHandler.on("requestError", data => {
console.error("request error", data)
})
;(async () => { ;(async () => {
await migrate.migrate(db) await migrate.migrate(db)
await discord.cloud.connect() await discord.cloud.connect()
console.log("Discord gateway started") console.log("Discord gateway started")
require("./matrix/power.js") await power.applyPower()
require("./stdin") require("./stdin")
})() })()

View file

@ -38,8 +38,7 @@ module.exports = {
}] }]
}, },
"m.room.avatar/": { "m.room.avatar/": {
discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024", url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"}
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
}, },
"m.room.power_levels/": { "m.room.power_levels/": {
events: { events: {

View file

@ -23,7 +23,8 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES
('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'), ('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'),
('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'), ('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon:cadence.moe'),
('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'), ('1109360903096369153', 'amanda', '_ooye_amanda', '@_ooye_amanda:cadence.moe'),
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'); ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '_pk_zoego', '_ooye__pk_zoego', '@_ooye__pk_zoego:cadence.moe'),
('320067006521147393', 'papiophidian', '_ooye_papiophidian', '@_ooye_papiophidian:cadence.moe');
INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES INSERT INTO sim_proxy (user_id, proxy_owner_id, displayname) VALUES
('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺'); ('43d378d5-1183-47dc-ab3c-d14e21c3fe58', '196188877885538304', 'Azalea &flwr; 🌺');
@ -125,19 +126,21 @@ INSERT INTO emoji (emoji_id, name, animated, mxc_url) VALUES
('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'), ('606664341298872324', 'online', 0, 'mxc://cadence.moe/LCEqjStXCxvRQccEkuslXEyZ'),
('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO'); ('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO');
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES
('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), ('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'), ('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc', 0),
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL), ('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL, 0),
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'), ('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL), ('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0),
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko'), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0),
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP'), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL), ('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL); ('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL, 0),
('!kLRqKKUQXcibIMtOpl:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 0),
('!BpMdOUkWWhFxmTrENV:cadence.moe', '@test_auto_invite:example.org', NULL, NULL, 100);
INSERT INTO member_power (mxid, room_id, power_level) VALUES INSERT INTO member_power (mxid, room_id, power_level) VALUES
('@test_auto_invite:example.org', '*', 100); ('@test_auto_invite:example.org', '*', 100);

View file

@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe"
reg.id = "baby" // don't actually take authenticated actions on the server reg.id = "baby" // don't actually take authenticated actions on the server
reg.as_token = "baby" reg.as_token = "baby"
reg.hs_token = "baby" reg.hs_token = "baby"
reg.ooye.invite = []
const sync = new HeatSync({watchFS: false}) const sync = new HeatSync({watchFS: false})
@ -116,6 +117,7 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
require("../matrix/kstate.test") require("../matrix/kstate.test")
require("../matrix/api.test") require("../matrix/api.test")
require("../matrix/file.test") require("../matrix/file.test")
require("../matrix/power.test")
require("../matrix/read-registration.test") require("../matrix/read-registration.test")
require("../matrix/txnid.test") require("../matrix/txnid.test")
require("../d2m/actions/create-room.test") require("../d2m/actions/create-room.test")