Compare commits
2 commits
5dbd79cf39
...
bad8c5b8c2
Author | SHA1 | Date | |
---|---|---|---|
bad8c5b8c2 | |||
65170c1282 |
16 changed files with 407 additions and 186 deletions
|
@ -1,25 +0,0 @@
|
||||||
id: de8c56117637cb5d9f4ac216f612dc2adb1de4c09ae8d13553f28c33a28147c7
|
|
||||||
hs_token: [a unique 64 character hex string]
|
|
||||||
as_token: [a unique 64 character hex string]
|
|
||||||
url: http://localhost:6693
|
|
||||||
sender_localpart: _ooye_bot
|
|
||||||
protocols:
|
|
||||||
- discord
|
|
||||||
namespaces:
|
|
||||||
users:
|
|
||||||
- exclusive: true
|
|
||||||
regex: '@_ooye_.*'
|
|
||||||
aliases:
|
|
||||||
- exclusive: true
|
|
||||||
regex: '#_ooye_.*'
|
|
||||||
rate_limited: false
|
|
||||||
ooye:
|
|
||||||
namespace_prefix: _ooye_
|
|
||||||
max_file_size: 5000000
|
|
||||||
server_name: [the part after the colon in your matrix id, like cadence.moe]
|
|
||||||
server_origin: [the full protocol and domain of your actual matrix server's location, with no trailing slash, like https://matrix.cadence.moe]
|
|
||||||
content_length_workaround: false
|
|
||||||
include_user_id_in_mxid: false
|
|
||||||
invite:
|
|
||||||
# uncomment this to auto-invite the named user to newly created spaces and mark them as admin (PL 100) everywhere
|
|
||||||
# - '@cadence:cadence.moe'
|
|
|
@ -7,6 +7,7 @@ const sqlite = require("better-sqlite3")
|
||||||
const {scheduler} = require("timers/promises")
|
const {scheduler} = require("timers/promises")
|
||||||
const {isDeepStrictEqual} = require("util")
|
const {isDeepStrictEqual} = require("util")
|
||||||
const {createServer} = require("http")
|
const {createServer} = require("http")
|
||||||
|
const {join} = require("path")
|
||||||
|
|
||||||
const {prompt} = require("enquirer")
|
const {prompt} = require("enquirer")
|
||||||
const Input = require("enquirer/lib/prompts/input")
|
const Input = require("enquirer/lib/prompts/input")
|
||||||
|
@ -208,7 +209,6 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) {
|
||||||
url: bridgeOriginResponse.bridge_origin,
|
url: bridgeOriginResponse.bridge_origin,
|
||||||
ooye: {
|
ooye: {
|
||||||
...template.ooye,
|
...template.ooye,
|
||||||
...serverNameResponse,
|
|
||||||
...bridgeOriginResponse,
|
...bridgeOriginResponse,
|
||||||
server_origin: serverOrigin,
|
server_origin: serverOrigin,
|
||||||
...discordTokenResponse,
|
...discordTokenResponse,
|
||||||
|
@ -335,8 +335,8 @@ async function validateHomeserverOrigin(serverUrlPrompt, url) {
|
||||||
}
|
}
|
||||||
// Upload those emojis to the chosen location
|
// Upload those emojis to the chosen location
|
||||||
db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id)
|
db.prepare("REPLACE INTO auto_emoji (name, emoji_id, guild_id) VALUES ('_', '_', ?)").run(guild.id)
|
||||||
await uploadAutoEmoji(discord.snow, guild, "L1", "docs/img/L1.png")
|
await uploadAutoEmoji(discord.snow, guild, "L1", join(__dirname, "../docs/img/L1.png"))
|
||||||
await uploadAutoEmoji(discord.snow, guild, "L2", "docs/img/L2.png")
|
await uploadAutoEmoji(discord.snow, guild, "L2", join(__dirname, "../docs/img/L2.png"))
|
||||||
}
|
}
|
||||||
console.log("✅ Emojis are ready...")
|
console.log("✅ Emojis are ready...")
|
||||||
|
|
||||||
|
|
|
@ -58,3 +58,13 @@ test("orm: from: join direction works", t => {
|
||||||
const hasNoOwnerInner = from("sim").join("sim_proxy", "user_id", "inner").select("user_id", "proxy_owner_id").where({sim_name: "crunch_god"}).get()
|
const hasNoOwnerInner = from("sim").join("sim_proxy", "user_id", "inner").select("user_id", "proxy_owner_id").where({sim_name: "crunch_god"}).get()
|
||||||
t.deepEqual(hasNoOwnerInner, undefined)
|
t.deepEqual(hasNoOwnerInner, undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("orm: select unsafe works (to select complex column names that can't be type verified)", t => {
|
||||||
|
const results = from("member_cache")
|
||||||
|
.join("member_power", "mxid")
|
||||||
|
.join("channel_room", "room_id") // only include rooms that are bridged
|
||||||
|
.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()
|
||||||
|
t.equal(results[0].power_level, 100)
|
||||||
|
})
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const DiscordTypes = require("discord-api-types/v10")
|
|
||||||
const Ty = require("../../types")
|
|
||||||
const {discord, sync, db, select, from, as} = require("../../passthrough")
|
|
||||||
const assert = require("assert/strict")
|
|
||||||
|
|
||||||
/** @type {import("../../matrix/api")} */
|
|
||||||
const api = sync.require("../../matrix/api")
|
|
||||||
|
|
||||||
/** @type {Map<string, Promise<{name: string, value: string}[]>>} spaceID -> list of rooms */
|
|
||||||
const cache = new Map()
|
|
||||||
/** @type {Map<string, string>} roomID -> spaceID */
|
|
||||||
const reverseCache = new Map()
|
|
||||||
|
|
||||||
// Manage clearing the cache
|
|
||||||
sync.addTemporaryListener(as, "type:m.room.name", /** @param {Ty.Event.StateOuter<Ty.Event.M_Room_Name>} event */ async event => {
|
|
||||||
if (event.state_key !== "") return
|
|
||||||
const roomID = event.room_id
|
|
||||||
const spaceID = reverseCache.get(roomID)
|
|
||||||
if (!spaceID) return
|
|
||||||
const childRooms = await cache.get(spaceID)
|
|
||||||
if (!childRooms) return
|
|
||||||
if (event.content.name) {
|
|
||||||
const found = childRooms.find(r => r.value === roomID)
|
|
||||||
if (!found) return
|
|
||||||
found.name = event.content.name
|
|
||||||
} else {
|
|
||||||
cache.set(spaceID, Promise.resolve(childRooms.filter(r => r.value !== roomID)))
|
|
||||||
reverseCache.delete(roomID)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Manage adding to the cache
|
|
||||||
async function getCachedHierarchy(spaceID) {
|
|
||||||
return cache.get(spaceID) || (() => {
|
|
||||||
const entry = (async () => {
|
|
||||||
const result = await api.getFullHierarchy(spaceID)
|
|
||||||
/** @type {{name: string, value: string}[]} */
|
|
||||||
const childRooms = []
|
|
||||||
for (const room of result) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return childRooms
|
|
||||||
})()
|
|
||||||
cache.set(spaceID, entry)
|
|
||||||
return entry
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction */
|
|
||||||
async function interactAutocomplete({id, token, data, guild_id}) {
|
|
||||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
|
||||||
if (!spaceID) {
|
|
||||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
name: `Error: This server needs to be bridged somewhere first...`,
|
|
||||||
value: "baby"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let rooms = await getCachedHierarchy(spaceID)
|
|
||||||
// @ts-ignore
|
|
||||||
rooms = rooms.filter(r => r.name.includes(data.options[0].value))
|
|
||||||
|
|
||||||
await discord.snow.interaction.createInteractionResponse(id, token, {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
|
||||||
data: {
|
|
||||||
choices: rooms
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */
|
|
||||||
async function interactSubmit({id, token, data, guild_id}) {
|
|
||||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
|
||||||
if (!spaceID) {
|
|
||||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: "Error: This server needs to be bridged somewhere first...",
|
|
||||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
|
||||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
|
||||||
data: {
|
|
||||||
content: "Valid input. This would do something but it isn't implemented yet.",
|
|
||||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIGuildInteraction} interaction */
|
|
||||||
async function interact(interaction) {
|
|
||||||
if (interaction.type === DiscordTypes.InteractionType.ApplicationCommandAutocomplete) {
|
|
||||||
return interactAutocomplete(interaction)
|
|
||||||
} else if (interaction.type === DiscordTypes.InteractionType.ApplicationCommand) {
|
|
||||||
// @ts-ignore
|
|
||||||
return interactSubmit(interaction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.interact = interact
|
|
|
@ -14,13 +14,14 @@ const api = sync.require("../../matrix/api")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
||||||
|
* @param {{api: typeof api}} di
|
||||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||||
*/
|
*/
|
||||||
async function _interact({data, channel, guild_id}) {
|
async function _interact({data, channel, guild_id}, {api}) {
|
||||||
// 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 {
|
if (!mxid) return {
|
||||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||||
|
@ -110,9 +111,10 @@ async function _interact({data, channel, guild_id}) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction
|
* @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction
|
||||||
|
* @param {{api: typeof api}} di
|
||||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||||
*/
|
*/
|
||||||
async function _interactButton({channel, message}) {
|
async function _interactButton({channel, message}, {api}) {
|
||||||
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()
|
||||||
|
@ -127,14 +129,16 @@ async function _interactButton({channel, message}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* c8 ignore start */
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */
|
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction */
|
||||||
async function interact(interaction) {
|
async function interact(interaction) {
|
||||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction, {api}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */
|
/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */
|
||||||
async function interactButton(interaction) {
|
async function interactButton(interaction) {
|
||||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction))
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction, {api}))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.interact = interact
|
module.exports.interact = interact
|
||||||
|
|
228
src/discord/interactions/invite.test.js
Normal file
228
src/discord/interactions/invite.test.js
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
const {test} = require("supertape")
|
||||||
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const {db, discord} = require("../../passthrough")
|
||||||
|
const {MatrixServerError} = require("../../matrix/mreq")
|
||||||
|
const {_interact, _interactButton} = require("./invite")
|
||||||
|
|
||||||
|
test("invite: checks for missing matrix ID", async t => {
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: []
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("0"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {})
|
||||||
|
t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: checks for invalid matrix ID", async t => {
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("0"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {})
|
||||||
|
t.equal(msg.data.content, "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: checks if channel exists or is autocreatable", async t => {
|
||||||
|
db.prepare("UPDATE guild_active SET autocreate = 0").run()
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("498323546729086986"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {})
|
||||||
|
t.equal(msg.data.content, "This channel isn't bridged, so you can't invite Matrix users yet. Try turning on automatic room-creation or link a Matrix room in the website.")
|
||||||
|
db.prepare("UPDATE guild_active SET autocreate = 1").run()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: checks if user is already invited to space", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getStateEvent: async (roomID, type, stateKey) => {
|
||||||
|
called++
|
||||||
|
t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(stateKey, "@cadence:cadence.moe")
|
||||||
|
return {
|
||||||
|
displayname: "cadence",
|
||||||
|
membership: "invite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "`@cadence:cadence.moe` already has an invite, which they haven't accepted yet.")
|
||||||
|
t.equal(called, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: invites if user is not in space", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getStateEvent: async (roomID, type, stateKey) => {
|
||||||
|
called++
|
||||||
|
t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(stateKey, "@cadence:cadence.moe")
|
||||||
|
throw new MatrixServerError("State event doesn't exist or something")
|
||||||
|
},
|
||||||
|
inviteToRoom: async (roomID, mxid) => {
|
||||||
|
called++
|
||||||
|
t.equal(roomID, "!jjWAGMeQdNrVZSSfvz:cadence.moe") // space ID
|
||||||
|
t.equal(mxid, "@cadence:cadence.moe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the server.")
|
||||||
|
t.equal(called, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: prompts to invite to room (if never joined)", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getStateEvent: async (roomID, type, stateKey) => {
|
||||||
|
called++
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(stateKey, "@cadence:cadence.moe")
|
||||||
|
if (roomID === "!jjWAGMeQdNrVZSSfvz:cadence.moe") { // space ID
|
||||||
|
return {
|
||||||
|
displayname: "cadence",
|
||||||
|
membership: "join"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new MatrixServerError("State event doesn't exist or something")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?")
|
||||||
|
t.equal(called, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: prompts to invite to room (if left)", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getStateEvent: async (roomID, type, stateKey) => {
|
||||||
|
called++
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(stateKey, "@cadence:cadence.moe")
|
||||||
|
if (roomID === "!jjWAGMeQdNrVZSSfvz:cadence.moe") { // space ID
|
||||||
|
return {
|
||||||
|
displayname: "cadence",
|
||||||
|
membership: "join"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
displayname: "cadence",
|
||||||
|
membership: "leave"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?")
|
||||||
|
t.equal(called, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite button: invites to room when button clicked", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interactButton({
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
message: {
|
||||||
|
content: "`@cadence:cadence.moe` is already in this server. Would you like to additionally invite them to this specific channel?"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
inviteToRoom: async (roomID, mxid) => {
|
||||||
|
called++
|
||||||
|
t.equal(roomID, "!kLRqKKUQXcibIMtOpl:cadence.moe") // room ID
|
||||||
|
t.equal(mxid, "@cadence:cadence.moe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "You invited `@cadence:cadence.moe` to the channel.")
|
||||||
|
t.equal(called, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("invite: no-op if in room and space", async t => {
|
||||||
|
let called = 0
|
||||||
|
const msg = await _interact({
|
||||||
|
data: {
|
||||||
|
options: [{
|
||||||
|
name: "user",
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
value: "@cadence:cadence.moe"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
channel: discord.channels.get("112760669178241024"),
|
||||||
|
guild_id: "112760669178241024"
|
||||||
|
}, {
|
||||||
|
api: {
|
||||||
|
getStateEvent: async (roomID, type, stateKey) => {
|
||||||
|
called++
|
||||||
|
t.equal(type, "m.room.member")
|
||||||
|
t.equal(stateKey, "@cadence:cadence.moe")
|
||||||
|
return {
|
||||||
|
displayname: "cadence",
|
||||||
|
membership: "join"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.equal(msg.data.content, "`@cadence:cadence.moe` is already in this server and this channel.")
|
||||||
|
t.equal(called, 2)
|
||||||
|
})
|
|
@ -7,7 +7,6 @@ const {id} = require("../../addbot")
|
||||||
const matrixInfo = sync.require("./interactions/matrix-info.js")
|
const matrixInfo = sync.require("./interactions/matrix-info.js")
|
||||||
const invite = sync.require("./interactions/invite.js")
|
const invite = sync.require("./interactions/invite.js")
|
||||||
const permissions = sync.require("./interactions/permissions.js")
|
const permissions = sync.require("./interactions/permissions.js")
|
||||||
const bridge = sync.require("./interactions/bridge.js")
|
|
||||||
const reactions = sync.require("./interactions/reactions.js")
|
const reactions = sync.require("./interactions/reactions.js")
|
||||||
const privacy = sync.require("./interactions/privacy.js")
|
const privacy = sync.require("./interactions/privacy.js")
|
||||||
|
|
||||||
|
@ -39,20 +38,6 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
||||||
name: "user"
|
name: "user"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, {
|
|
||||||
name: "bridge",
|
|
||||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
|
||||||
type: DiscordTypes.ApplicationCommandType.ChatInput,
|
|
||||||
description: "Start bridging this channel to a Matrix room",
|
|
||||||
default_member_permissions: String(DiscordTypes.PermissionFlagsBits.ManageChannels),
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
type: DiscordTypes.ApplicationCommandOptionType.String,
|
|
||||||
description: "Destination room to bridge to",
|
|
||||||
name: "room",
|
|
||||||
autocomplete: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
name: "privacy",
|
name: "privacy",
|
||||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||||
|
@ -94,8 +79,6 @@ async function dispatchInteraction(interaction) {
|
||||||
await permissions.interact(interaction)
|
await permissions.interact(interaction)
|
||||||
} else if (interactionId === "permissions_edit") {
|
} else if (interactionId === "permissions_edit") {
|
||||||
await permissions.interactEdit(interaction)
|
await permissions.interactEdit(interaction)
|
||||||
} else if (interactionId === "bridge") {
|
|
||||||
await bridge.interact(interaction)
|
|
||||||
} else if (interactionId === "Reactions") {
|
} else if (interactionId === "Reactions") {
|
||||||
await reactions.interact(interaction)
|
await reactions.interact(interaction)
|
||||||
} else if (interactionId === "privacy") {
|
} else if (interactionId === "privacy") {
|
||||||
|
|
|
@ -113,7 +113,7 @@ function isWebhookMessage(message) {
|
||||||
* @param {Pick<DiscordTypes.APIMessage, "flags">} message
|
* @param {Pick<DiscordTypes.APIMessage, "flags">} message
|
||||||
*/
|
*/
|
||||||
function isEphemeralMessage(message) {
|
function isEphemeralMessage(message) {
|
||||||
return message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral)
|
return Boolean(message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} snowflake */
|
/** @param {string} snowflake */
|
||||||
|
|
|
@ -84,6 +84,67 @@ test("getPermissions: channel overwrite to allow role works", t => {
|
||||||
t.equal((permissions & want), want)
|
t.equal((permissions & want), want)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("getPermissions: channel overwrite to allow user works", t => {
|
||||||
|
const guildRoles = [
|
||||||
|
{
|
||||||
|
version: 1695412489043,
|
||||||
|
unicode_emoji: null,
|
||||||
|
tags: {},
|
||||||
|
position: 0,
|
||||||
|
permissions: "559623605571137",
|
||||||
|
name: "@everyone",
|
||||||
|
mentionable: false,
|
||||||
|
managed: false,
|
||||||
|
id: "1154868424724463687",
|
||||||
|
icon: null,
|
||||||
|
hoist: false,
|
||||||
|
flags: 0,
|
||||||
|
color: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 1695412604262,
|
||||||
|
unicode_emoji: null,
|
||||||
|
tags: { bot_id: "466378653216014359" },
|
||||||
|
position: 1,
|
||||||
|
permissions: "536995904",
|
||||||
|
name: "PluralKit",
|
||||||
|
mentionable: false,
|
||||||
|
managed: true,
|
||||||
|
id: "1154868908336099444",
|
||||||
|
icon: null,
|
||||||
|
hoist: false,
|
||||||
|
flags: 0,
|
||||||
|
color: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 1698778936921,
|
||||||
|
unicode_emoji: null,
|
||||||
|
tags: {},
|
||||||
|
position: 1,
|
||||||
|
permissions: "536870912",
|
||||||
|
name: "web hookers",
|
||||||
|
mentionable: false,
|
||||||
|
managed: false,
|
||||||
|
id: "1168988246680801360",
|
||||||
|
icon: null,
|
||||||
|
hoist: false,
|
||||||
|
flags: 0,
|
||||||
|
color: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const userRoles = []
|
||||||
|
const userID = "353373325575323648"
|
||||||
|
const overwrites = [
|
||||||
|
{ type: 0, id: "1154868908336099444", deny: "0", allow: "1024" },
|
||||||
|
{ type: 0, id: "1154868424724463687", deny: "1024", allow: "0" },
|
||||||
|
{ type: 0, id: "1168988246680801360", deny: "0", allow: "1024" },
|
||||||
|
{ type: 1, id: "353373325575323648", deny: "0", allow: "1024" }
|
||||||
|
]
|
||||||
|
const permissions = utils.getPermissions(userRoles, guildRoles, userID, overwrites)
|
||||||
|
const want = BigInt(1 << 10 | 1 << 16)
|
||||||
|
t.equal((permissions & want), want)
|
||||||
|
})
|
||||||
|
|
||||||
test("hasSomePermissions: detects the permission", t => {
|
test("hasSomePermissions: detects the permission", t => {
|
||||||
const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers
|
const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers
|
||||||
const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"])
|
const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||||
|
@ -107,3 +168,15 @@ test("hasAllPermissions: doesn't detect not the permissions", t => {
|
||||||
const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"])
|
const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||||
t.equal(canRemoveMembers, false)
|
t.equal(canRemoveMembers, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("isEphemeralMessage: detects ephemeral message", t => {
|
||||||
|
t.equal(utils.isEphemeralMessage(data.special_message.ephemeral_message), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isEphemeralMessage: doesn't detect normal message", t => {
|
||||||
|
t.equal(utils.isEphemeralMessage(data.message.simple_plaintext), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("getPublicUrlForCdn: no-op on non-discord URL", t => {
|
||||||
|
t.equal(utils.getPublicUrlForCdn("https://cadence.moe"), "https://cadence.moe")
|
||||||
|
})
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const {test} = require("supertape")
|
|
||||||
const power = require("./power")
|
|
||||||
|
|
||||||
test("power: get affected rooms", t => {
|
|
||||||
t.deepEqual(power._getAffectedRooms(), [{
|
|
||||||
mxid: "@test_auto_invite:example.org",
|
|
||||||
power_level: 100,
|
|
||||||
room_id: "!kLRqKKUQXcibIMtOpl:cadence.moe",
|
|
||||||
}])
|
|
||||||
})
|
|
|
@ -9,7 +9,7 @@ const registrationFilePath = path.join(process.cwd(), "registration.yaml")
|
||||||
|
|
||||||
/** @param {import("../types").AppServiceRegistrationConfig} reg */
|
/** @param {import("../types").AppServiceRegistrationConfig} reg */
|
||||||
function checkRegistration(reg) {
|
function checkRegistration(reg) {
|
||||||
reg["ooye"].invite = (reg.ooye.invite || []).filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line
|
reg["ooye"].invite = reg.ooye.invite.filter(mxid => mxid.endsWith(`:${reg.ooye.server_name}`)) // one day I will understand why typescript disagrees with dot notation on this line
|
||||||
assert(reg.ooye?.max_file_size)
|
assert(reg.ooye?.max_file_size)
|
||||||
assert(reg.ooye?.namespace_prefix)
|
assert(reg.ooye?.namespace_prefix)
|
||||||
assert(reg.ooye?.server_name)
|
assert(reg.ooye?.server_name)
|
||||||
|
@ -19,6 +19,7 @@ function checkRegistration(reg) {
|
||||||
assert.match(reg.url, /^https?:/, "url must start with http:// or https://")
|
assert.match(reg.url, /^https?:/, "url must start with http:// or https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* c8 ignore next 4 */
|
||||||
/** @param {import("../types").AppServiceRegistrationConfig} reg */
|
/** @param {import("../types").AppServiceRegistrationConfig} reg */
|
||||||
function writeRegistration(reg) {
|
function writeRegistration(reg) {
|
||||||
fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2))
|
fs.writeFileSync(registrationFilePath, JSON.stringify(reg, null, 2))
|
||||||
|
@ -52,6 +53,7 @@ function getTemplateRegistration(serverName) {
|
||||||
socket: 6693,
|
socket: 6693,
|
||||||
ooye: {
|
ooye: {
|
||||||
namespace_prefix,
|
namespace_prefix,
|
||||||
|
server_name: serverName,
|
||||||
max_file_size: 5000000,
|
max_file_size: 5000000,
|
||||||
content_length_workaround: false,
|
content_length_workaround: false,
|
||||||
include_user_id_in_mxid: false,
|
include_user_id_in_mxid: false,
|
||||||
|
@ -66,6 +68,8 @@ function readRegistration() {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(registrationFilePath, "utf8")
|
const content = fs.readFileSync(registrationFilePath, "utf8")
|
||||||
result = JSON.parse(content)
|
result = JSON.parse(content)
|
||||||
|
result.ooye.invite ||= []
|
||||||
|
/* c8 ignore next */
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const tryToCatch = require("try-to-catch")
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {reg} = require("./read-registration")
|
const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration")
|
||||||
|
|
||||||
test("reg: has necessary parameters", t => {
|
test("reg: has necessary parameters", t => {
|
||||||
const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"]
|
const propertiesToCheck = ["sender_localpart", "id", "as_token", "ooye"]
|
||||||
|
@ -8,3 +11,19 @@ test("reg: has necessary parameters", t => {
|
||||||
propertiesToCheck
|
propertiesToCheck
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("check: passes on sample", t => {
|
||||||
|
checkRegistration(reg)
|
||||||
|
t.pass("all assertions passed")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("check: fails on template as template is missing some required values that are gathered during setup", t => {
|
||||||
|
let err
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
checkRegistration(getTemplateRegistration("cadence.moe"))
|
||||||
|
} catch (e) {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
t.ok(err, "one of the assertions failed as expected")
|
||||||
|
})
|
||||||
|
|
7
src/types.d.ts
vendored
7
src/types.d.ts
vendored
|
@ -55,9 +55,10 @@ export type InitialAppServiceRegistrationConfig = {
|
||||||
socket?: string | number,
|
socket?: string | number,
|
||||||
ooye: {
|
ooye: {
|
||||||
namespace_prefix: string
|
namespace_prefix: string
|
||||||
max_file_size: number,
|
server_name: string
|
||||||
content_length_workaround: boolean,
|
max_file_size: number
|
||||||
invite: string[],
|
content_length_workaround: boolean
|
||||||
|
invite: string[]
|
||||||
include_user_id_in_mxid: boolean
|
include_user_id_in_mxid: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
test/data.js
49
test/data.js
|
@ -4128,7 +4128,54 @@ module.exports = {
|
||||||
guild_id: "112760669178241024"
|
guild_id: "112760669178241024"
|
||||||
},
|
},
|
||||||
position: 0
|
position: 0
|
||||||
}
|
},
|
||||||
|
ephemeral_message: {
|
||||||
|
webhook_id: "684280192553844747",
|
||||||
|
type: 20,
|
||||||
|
tts: false,
|
||||||
|
timestamp: "2024-09-29T11:22:04.865000+00:00",
|
||||||
|
position: 0,
|
||||||
|
pinned: false,
|
||||||
|
nonce: "1289910062243905536",
|
||||||
|
mentions: [],
|
||||||
|
mention_roles: [],
|
||||||
|
mention_everyone: false,
|
||||||
|
interaction_metadata: {
|
||||||
|
user: {baby: true},
|
||||||
|
type: 2,
|
||||||
|
name: "invite",
|
||||||
|
id: "1289910063691206717",
|
||||||
|
command_type: 1,
|
||||||
|
authorizing_integration_owners: {baby: true}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
user: {baby: true},
|
||||||
|
type: 2,
|
||||||
|
name: "invite",
|
||||||
|
id: "1289910063691206717"
|
||||||
|
},
|
||||||
|
id: "1289910064995504182",
|
||||||
|
flags: 64,
|
||||||
|
embeds: [],
|
||||||
|
edited_timestamp: null,
|
||||||
|
content: "`@cadence:cadence.moe` is already in this server and this channel.",
|
||||||
|
components: [],
|
||||||
|
channel_id: "1100319550446252084",
|
||||||
|
author: {
|
||||||
|
username: "Matrix Bridge",
|
||||||
|
public_flags: 0,
|
||||||
|
id: "684280192553844747",
|
||||||
|
global_name: null,
|
||||||
|
discriminator: "5728",
|
||||||
|
clan: null,
|
||||||
|
bot: true,
|
||||||
|
avatar_decoration_data: null,
|
||||||
|
avatar: "48ae3c24f2a6ec5c60c41bdabd904018"
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
application_id: "684280192553844747"
|
||||||
|
},
|
||||||
|
shard_id: 0
|
||||||
},
|
},
|
||||||
interaction_message: {
|
interaction_message: {
|
||||||
thinking_interaction_without_bot_user: {
|
thinking_interaction_without_bot_user: {
|
||||||
|
|
|
@ -3,6 +3,9 @@ BEGIN TRANSACTION;
|
||||||
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
|
INSERT INTO guild_space (guild_id, space_id, privacy_level) VALUES
|
||||||
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0);
|
('112760669178241024', '!jjWAGMeQdNrVZSSfvz:cadence.moe', 0);
|
||||||
|
|
||||||
|
INSERT INTO guild_active (guild_id, autocreate) VALUES
|
||||||
|
('112760669178241024', 1);
|
||||||
|
|
||||||
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
|
INSERT INTO channel_room (channel_id, room_id, name, nick, thread_parent, custom_avatar) VALUES
|
||||||
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
|
('112760669178241024', '!kLRqKKUQXcibIMtOpl:cadence.moe', 'heave', 'main', NULL, NULL),
|
||||||
('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL),
|
('497161350934560778', '!CzvdIdUQXgUjDVKxeU:cadence.moe', 'amanda-spam', NULL, NULL, NULL),
|
||||||
|
|
|
@ -23,8 +23,8 @@ 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.bridge_origin = "https://bridge.example.org"
|
reg.ooye.bridge_origin = "https://bridge.example.org"
|
||||||
reg.ooye.invite = []
|
|
||||||
|
|
||||||
|
/** @type {import("heatsync").default} */ // @ts-ignore
|
||||||
const sync = new HeatSync({watchFS: false})
|
const sync = new HeatSync({watchFS: false})
|
||||||
|
|
||||||
const discord = {
|
const discord = {
|
||||||
|
@ -35,6 +35,7 @@ const discord = {
|
||||||
id: "684280192553844747"
|
id: "684280192553844747"
|
||||||
},
|
},
|
||||||
channels: new Map([
|
channels: new Map([
|
||||||
|
[data.channel.general.id, data.channel.general],
|
||||||
["497161350934560778", {
|
["497161350934560778", {
|
||||||
guild_id: "497159726455455754"
|
guild_id: "497159726455455754"
|
||||||
}],
|
}],
|
||||||
|
@ -117,7 +118,6 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/matrix/kstate.test")
|
require("../src/matrix/kstate.test")
|
||||||
require("../src/matrix/api.test")
|
require("../src/matrix/api.test")
|
||||||
require("../src/matrix/file.test")
|
require("../src/matrix/file.test")
|
||||||
require("../src/matrix/power.test")
|
|
||||||
require("../src/matrix/read-registration.test")
|
require("../src/matrix/read-registration.test")
|
||||||
require("../src/matrix/txnid.test")
|
require("../src/matrix/txnid.test")
|
||||||
require("../src/d2m/actions/create-room.test")
|
require("../src/d2m/actions/create-room.test")
|
||||||
|
@ -136,4 +136,5 @@ file._actuallyUploadDiscordFileToMxc = function(url, res) { throw new Error(`Not
|
||||||
require("../src/m2d/converters/event-to-message.test")
|
require("../src/m2d/converters/event-to-message.test")
|
||||||
require("../src/m2d/converters/utils.test")
|
require("../src/m2d/converters/utils.test")
|
||||||
require("../src/m2d/converters/emoji-sheet.test")
|
require("../src/m2d/converters/emoji-sheet.test")
|
||||||
|
require("../src/discord/interactions/invite.test")
|
||||||
})()
|
})()
|
||||||
|
|
Loading…
Reference in a new issue