Compare commits
10 commits
3a84658e8b
...
5d91f999f2
Author | SHA1 | Date | |
---|---|---|---|
5d91f999f2 | |||
2c27879afb | |||
71c553a9cf | |||
607fd3808a | |||
818311bcb4 | |||
a6c961984d | |||
42bfd034cf | |||
4b7593d630 | |||
78a17b2de9 | |||
ddb211f8f3 |
23 changed files with 205 additions and 100 deletions
|
@ -120,8 +120,7 @@ async function channelToKState(channel, guild) {
|
|||
if (customAvatar) {
|
||||
avatarEventContent.url = customAvatar
|
||||
} else if (guild.icon) {
|
||||
avatarEventContent.discord_path = 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
|
||||
avatarEventContent.url = {$url: file.guildIcon(guild)}
|
||||
}
|
||||
|
||||
let history_visibility = PRIVACY_ENUMS.ROOM_HISTORY_VISIBILITY[privacyLevel]
|
||||
|
|
|
@ -65,7 +65,6 @@ async function guildToKState(guild, privacyLevel) {
|
|||
"m.room.name/": {name: guild.name},
|
||||
"m.room.avatar/": {
|
||||
$if: guild.icon,
|
||||
discord_path: file.guildIcon(guild),
|
||||
url: {$url: file.guildIcon(guild)}
|
||||
},
|
||||
"m.room.guest_access/": {guest_access: createRoom.PRIVACY_ENUMS.GUEST_ACCESS[privacyLevel]},
|
||||
|
|
|
@ -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))),
|
||||
{
|
||||
"m.room.avatar/": {
|
||||
discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
|
||||
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
|
||||
},
|
||||
"m.room.guest_access/": {
|
||||
|
|
|
@ -6,6 +6,13 @@ const Ty = require("../../types")
|
|||
test("message2event embeds: nothing but a field", async t => {
|
||||
const events = await messageToEvent(data.message_with_embeds.nothing_but_a_field, data.guild.general, {})
|
||||
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",
|
||||
"m.mentions": {},
|
||||
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 => {
|
||||
const events = await messageToEvent(data.message_with_embeds.title_without_url, data.guild.general)
|
||||
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",
|
||||
msgtype: "m.notice",
|
||||
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 => {
|
||||
const events = await messageToEvent(data.message_with_embeds.url_without_title, data.guild.general)
|
||||
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",
|
||||
msgtype: "m.notice",
|
||||
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 => {
|
||||
const events = await messageToEvent(data.message_with_embeds.author_without_url, data.guild.general)
|
||||
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",
|
||||
msgtype: "m.notice",
|
||||
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 => {
|
||||
const events = await messageToEvent(data.message_with_embeds.author_url_without_name, data.guild.general)
|
||||
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",
|
||||
msgtype: "m.notice",
|
||||
body: "| I condone pirating music!",
|
||||
|
|
|
@ -32,7 +32,10 @@ function getDiscordParseCallbacks(message, guild, useHTML) {
|
|||
/** @param {{id: string, type: "discordUser"}} node */
|
||||
user: node => {
|
||||
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) {
|
||||
return `<a href="https://matrix.to/#/${mxid}">@${username}</a>`
|
||||
} 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[]}}
|
||||
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.
|
||||
// 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 => {
|
||||
const id = match[3]
|
||||
const name = match[2]
|
||||
|
|
|
@ -57,6 +57,9 @@ class DiscordClient {
|
|||
addEventLogger("error", "Error")
|
||||
addEventLogger("disconnected", "Disconnected")
|
||||
addEventLogger("ready", "Ready")
|
||||
this.snow.requestHandler.on("requestError", (requestID, error) => {
|
||||
console.error("request error:", error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -181,7 +181,7 @@ const utils = {
|
|||
|
||||
} catch (e) {
|
||||
// Let OOYE try to handle errors too
|
||||
eventDispatcher.onError(client, e, message)
|
||||
await eventDispatcher.onError(client, e, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ module.exports = {
|
|||
* @param {Error} e
|
||||
* @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(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:`)
|
||||
|
@ -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("", `<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(),
|
||||
"moe.cadence.ooye.error": {
|
||||
source: "discord",
|
||||
|
|
1
db/orm-defs.d.ts
vendored
1
db/orm-defs.d.ts
vendored
|
@ -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 Merge<U> = {[x in AllKeys<U>]: PickTypeOf<U, x>}
|
||||
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]}
|
||||
|
|
|
@ -8,7 +8,7 @@ const U = require("./orm-defs")
|
|||
* @template {keyof U.Models[Table]} Col
|
||||
* @param {Table} table
|
||||
* @param {Col[] | Col} cols
|
||||
* @param {Partial<U.Models[Table]>} where
|
||||
* @param {Partial<U.Numberish<U.Models[Table]>>} where
|
||||
* @param {string} [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) {
|
||||
const wheres = Object.entries(conditions).map(([col, value]) => {
|
||||
|
|
|
@ -39,7 +39,7 @@ async function getCachedHierarchy(spaceID) {
|
|||
/** @type {{name: string, value: string}[]} */
|
||||
const childRooms = []
|
||||
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})
|
||||
reverseCache.set(room.room_id, spaceID)
|
||||
}
|
||||
|
|
|
@ -7,31 +7,34 @@ const {discord, sync, db, select, from} = require("../../passthrough")
|
|||
/** @type {import("../../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
|
||||
const spaceID = select("guild_space", "space_id", {guild_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,
|
||||
data: {
|
||||
content: "This server isn't bridged to Matrix, so you can't invite Matrix users.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Get named MXID
|
||||
/** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore
|
||||
const options = data.options
|
||||
const input = options?.[0].value || ""
|
||||
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,
|
||||
data: {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check for existing invite to the space
|
||||
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)
|
||||
} catch (e) {}
|
||||
if (spaceMember && spaceMember.membership === "invite") {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Invite Matrix user if not in space
|
||||
if (!spaceMember || spaceMember.membership !== "join") {
|
||||
await api.inviteToRoom(spaceID, mxid)
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `You invited \`${mxid}\` to the server.`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
} catch (e) {}
|
||||
if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
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.
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${mxid}\` is already in this server and this channel.`,
|
||||
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]
|
||||
assert(mxid)
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
await api.inviteToRoom(roomID, mxid)
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.UpdateMessage,
|
||||
data: {
|
||||
content: `You invited \`${mxid}\` to the channel.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||
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.interactButton = interactButton
|
||||
module.exports._interact = _interact
|
||||
module.exports._interactButton = _interactButton
|
||||
|
|
|
@ -7,7 +7,8 @@ const {discord, sync, db, select, from} = require("../../passthrough")
|
|||
const api = sync.require("../../matrix/api")
|
||||
|
||||
/** @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")
|
||||
.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
|
||||
const userID = data.resolved.messages[data.target_id].author.id
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `This message was bridged 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}\``,
|
||||
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.`
|
||||
+ idInfo,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
|
@ -37,9 +41,8 @@ async function interact({id, token, data}) {
|
|||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `This message was bridged from [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix.`
|
||||
+ `\nIt was originally sent by [${event.sender}](<https://matrix.to/#/${event.sender}>).`
|
||||
+ `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``,
|
||||
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.`
|
||||
+ idInfo,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,23 +5,27 @@ const Ty = require("../../types")
|
|||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
const assert = require("assert/strict")
|
||||
|
||||
|
||||
/** @type {import("../../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()
|
||||
assert(row)
|
||||
|
||||
// Can't operate on Discord users
|
||||
if (row.source === 1) { // discord
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `This command is only meaningful for Matrix users.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (userPower >= 100) {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${sender}\` has administrator permissions. This cannot be edited.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
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
|
||||
const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1]
|
||||
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
|
||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
||||
assert(spaceID)
|
||||
|
||||
// Do it
|
||||
const permission = data.values[0]
|
||||
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...)
|
||||
await api.setUserPowerCascade(spaceID, mxid, power)
|
||||
|
||||
// ACK
|
||||
await discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.DeferredMessageUpdate
|
||||
await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, {
|
||||
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.interactEdit = interactEdit
|
||||
module.exports._interact = _interact
|
||||
|
|
|
@ -48,23 +48,23 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
|||
}])
|
||||
|
||||
async function dispatchInteraction(interaction) {
|
||||
const id = interaction.data.custom_id || interaction.data.name
|
||||
const interactionId = interaction.data.custom_id || interaction.data.name
|
||||
try {
|
||||
console.log(interaction)
|
||||
if (id === "Matrix info") {
|
||||
if (interactionId === "Matrix info") {
|
||||
await matrixInfo.interact(interaction)
|
||||
} else if (id === "invite") {
|
||||
} else if (interactionId === "invite") {
|
||||
await invite.interact(interaction)
|
||||
} else if (id === "invite_channel") {
|
||||
} else if (interactionId === "invite_channel") {
|
||||
await invite.interactButton(interaction)
|
||||
} else if (id === "Permissions") {
|
||||
} else if (interactionId === "Permissions") {
|
||||
await permissions.interact(interaction)
|
||||
} else if (id === "permissions_edit") {
|
||||
} else if (interactionId === "permissions_edit") {
|
||||
await permissions.interactEdit(interaction)
|
||||
} else if (id === "bridge") {
|
||||
} else if (interactionId === "bridge") {
|
||||
await bridge.interact(interaction)
|
||||
} else {
|
||||
throw new Error(`Unknown interaction ${id}`)
|
||||
throw new Error(`Unknown interaction ${interactionId}`)
|
||||
}
|
||||
} catch (e) {
|
||||
let stackLines = null
|
||||
|
@ -75,14 +75,11 @@ async function dispatchInteraction(interaction) {
|
|||
stackLines = stackLines.slice(0, cloudstormLine - 2)
|
||||
}
|
||||
}
|
||||
discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `Interaction failed: **${id}**`
|
||||
await discord.snow.interaction.createFollowupMessage(id, interaction.token, {
|
||||
content: `Interaction failed: **${interactionId}**`
|
||||
+ `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\``
|
||||
+ `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,8 +97,7 @@ function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
|
|||
* @param {DiscordTypes.APIMessage} message
|
||||
*/
|
||||
function isWebhookMessage(message) {
|
||||
const isInteractionResponse = message.type === 20
|
||||
return message.webhook_id && !isInteractionResponse
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,7 +25,6 @@ async function deleteMessage(event) {
|
|||
*/
|
||||
async function removeReaction(event) {
|
||||
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()
|
||||
if (!row) return
|
||||
await discord.snow.channel.deleteReactionSelf(row.channel_id, row.message_id, row.encoded_emoji)
|
||||
|
|
|
@ -260,6 +260,21 @@ async function setUserPower(roomID, mxid, power) {
|
|||
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.register = register
|
||||
module.exports.createRoom = createRoom
|
||||
|
@ -281,3 +296,4 @@ module.exports.sendTyping = sendTyping
|
|||
module.exports.profileSetDisplayname = profileSetDisplayname
|
||||
module.exports.profileSetAvatarUrl = profileSetAvatarUrl
|
||||
module.exports.setUserPower = setUserPower
|
||||
module.exports.setUserPowerCascade = setUserPowerCascade
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
const {db, from} = require("../passthrough")
|
||||
const api = require("./api")
|
||||
const reg = require("./read-registration")
|
||||
const ks = require("./kstate")
|
||||
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, "*")
|
||||
}
|
||||
|
||||
// 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")
|
||||
/** Apply global power level requests across ALL rooms where the member cache entry exists but the power level has not been applied yet. */
|
||||
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")
|
||||
.selectUnsafe("mxid", "member_cache.room_id", "member_power.power_level")
|
||||
.all()
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
async function applyPower() {
|
||||
const rows = _getAffectedRooms()
|
||||
for (const row of rows) {
|
||||
const kstate = await roomToKState(row.room_id)
|
||||
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.
|
||||
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
|
||||
|
|
7
start.js
7
start.js
|
@ -25,17 +25,14 @@ const orm = sync.require("./db/orm")
|
|||
passthrough.from = orm.from
|
||||
passthrough.select = orm.select
|
||||
|
||||
const power = require("./matrix/power.js")
|
||||
sync.require("./m2d/event-dispatcher")
|
||||
|
||||
discord.snow.requestHandler.on("requestError", data => {
|
||||
console.error("request error", data)
|
||||
})
|
||||
|
||||
;(async () => {
|
||||
await migrate.migrate(db)
|
||||
await discord.cloud.connect()
|
||||
console.log("Discord gateway started")
|
||||
require("./matrix/power.js")
|
||||
await power.applyPower()
|
||||
|
||||
require("./stdin")
|
||||
})()
|
||||
|
|
|
@ -38,8 +38,7 @@ module.exports = {
|
|||
}]
|
||||
},
|
||||
"m.room.avatar/": {
|
||||
discord_path: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024",
|
||||
url: "mxc://cadence.moe/zKXGZhmImMHuGQZWJEFKJbsF"
|
||||
url: {$url: "/icons/112760669178241024/a_f83622e09ead74f0c5c527fe241f8f8c.png?size=1024"}
|
||||
},
|
||||
"m.room.power_levels/": {
|
||||
events: {
|
||||
|
|
|
@ -23,7 +23,8 @@ INSERT INTO sim (user_id, sim_name, localpart, mxid) VALUES
|
|||
('114147806469554185', 'extremity', '_ooye_extremity', '@_ooye_extremity:cadence.moe'),
|
||||
('111604486476181504', 'kyuugryphon', '_ooye_kyuugryphon', '@_ooye_kyuugryphon: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
|
||||
('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'),
|
||||
('288858540888686602', 'upstinky', 0, 'mxc://cadence.moe/mwZaCtRGAQQyOItagDeCocEO');
|
||||
|
||||
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url) VALUES
|
||||
('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
|
||||
('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc'),
|
||||
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
||||
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL),
|
||||
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
||||
('!maggESguZBqGBZtSnr:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
||||
('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU'),
|
||||
('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko'),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP'),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL),
|
||||
('!BnKuBPCvyfOkhcUjEu:cadence.moe', '@ami:the-apothecary.club', 'Ami (she/her)', NULL);
|
||||
INSERT INTO member_cache (room_id, mxid, displayname, avatar_url, power_level) VALUES
|
||||
('!kLRqKKUQXcibIMtOpl:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
|
||||
('!BpMdOUkWWhFxmTrENV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'malformed mxc', 0),
|
||||
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
|
||||
('!fGgIymcYWOqjbSRUdV:cadence.moe', '@rnl:cadence.moe', 'RNL', NULL, 0),
|
||||
('!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', 0),
|
||||
('!CzvdIdUQXgUjDVKxeU:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', 'mxc://cadence.moe/azCAhThKTojXSZJRoWwZmhvU', 0),
|
||||
('!cBxtVRxDlZvSVhJXVK:cadence.moe', '@Milan:tchncs.de', 'Milan', NULL, 0),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@ampflower:matrix.org', 'Ampflower 🌺', 'mxc://cadence.moe/PRfhXYBTOalvgQYtmCLeUXko', 0),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@aflower:syndicated.gay', 'Rose', 'mxc://syndicated.gay/ZkBUPXCiXTjdJvONpLJmcbKP', 0),
|
||||
('!TqlyQmifxGUggEmdBN:cadence.moe', '@cadence:cadence.moe', 'cadence [they]', NULL, 0),
|
||||
('!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
|
||||
('@test_auto_invite:example.org', '*', 100);
|
||||
|
|
|
@ -23,6 +23,7 @@ reg.ooye.server_name = "cadence.moe"
|
|||
reg.id = "baby" // don't actually take authenticated actions on the server
|
||||
reg.as_token = "baby"
|
||||
reg.hs_token = "baby"
|
||||
reg.ooye.invite = []
|
||||
|
||||
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/api.test")
|
||||
require("../matrix/file.test")
|
||||
require("../matrix/power.test")
|
||||
require("../matrix/read-registration.test")
|
||||
require("../matrix/txnid.test")
|
||||
require("../d2m/actions/create-room.test")
|
||||
|
|
Loading…
Reference in a new issue