Compare commits
No commits in common. "bad8c5b8c249f9060aea44d903f1ca47cbadf8b0" and "5dbd79cf3920f8ebffb1753158e716bf28193f1d" have entirely different histories.
bad8c5b8c2
...
5dbd79cf39
16 changed files with 186 additions and 407 deletions
25
registration.example.yaml
Normal file
25
registration.example.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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,7 +7,6 @@ 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")
|
||||||
|
@ -209,6 +208,7 @@ 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", join(__dirname, "../docs/img/L1.png"))
|
await uploadAutoEmoji(discord.snow, guild, "L1", "docs/img/L1.png")
|
||||||
await uploadAutoEmoji(discord.snow, guild, "L2", join(__dirname, "../docs/img/L2.png"))
|
await uploadAutoEmoji(discord.snow, guild, "L2", "docs/img/L2.png")
|
||||||
}
|
}
|
||||||
console.log("✅ Emojis are ready...")
|
console.log("✅ Emojis are ready...")
|
||||||
|
|
||||||
|
|
|
@ -58,13 +58,3 @@ 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)
|
|
||||||
})
|
|
||||||
|
|
115
src/discord/interactions/bridge.js
Normal file
115
src/discord/interactions/bridge.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// @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,14 +14,13 @@ 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}, {api}) {
|
async function _interact({data, channel, guild_id}) {
|
||||||
// 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,
|
||||||
|
@ -111,10 +110,9 @@ async function _interact({data, channel, guild_id}, {api}) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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}, {api}) {
|
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()
|
||||||
|
@ -129,16 +127,14 @@ async function _interactButton({channel, message}, {api}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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, {api}))
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @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, {api}))
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.interact = interact
|
module.exports.interact = interact
|
||||||
|
|
|
@ -1,228 +0,0 @@
|
||||||
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,6 +7,7 @@ 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")
|
||||||
|
|
||||||
|
@ -38,6 +39,20 @@ 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],
|
||||||
|
@ -79,6 +94,8 @@ 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 Boolean(message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral))
|
return message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} snowflake */
|
/** @param {string} snowflake */
|
||||||
|
|
|
@ -84,67 +84,6 @@ 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"])
|
||||||
|
@ -168,15 +107,3 @@ 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")
|
|
||||||
})
|
|
||||||
|
|
12
src/matrix/power.test.js
Normal file
12
src/matrix/power.test.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// @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,7 +19,6 @@ 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))
|
||||||
|
@ -53,7 +52,6 @@ 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,
|
||||||
|
@ -68,8 +66,6 @@ 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,8 +1,5 @@
|
||||||
// @ts-check
|
|
||||||
|
|
||||||
const tryToCatch = require("try-to-catch")
|
|
||||||
const {test} = require("supertape")
|
const {test} = require("supertape")
|
||||||
const {reg, checkRegistration, getTemplateRegistration} = require("./read-registration")
|
const {reg} = 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"]
|
||||||
|
@ -11,19 +8,3 @@ 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,10 +55,9 @@ export type InitialAppServiceRegistrationConfig = {
|
||||||
socket?: string | number,
|
socket?: string | number,
|
||||||
ooye: {
|
ooye: {
|
||||||
namespace_prefix: string
|
namespace_prefix: string
|
||||||
server_name: string
|
max_file_size: number,
|
||||||
max_file_size: number
|
content_length_workaround: boolean,
|
||||||
content_length_workaround: boolean
|
invite: string[],
|
||||||
invite: string[]
|
|
||||||
include_user_id_in_mxid: boolean
|
include_user_id_in_mxid: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
test/data.js
49
test/data.js
|
@ -4128,54 +4128,7 @@ 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,9 +3,6 @@ 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,7 +35,6 @@ 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"
|
||||||
}],
|
}],
|
||||||
|
@ -118,6 +117,7 @@ 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,5 +136,4 @@ 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