Compare commits

...

2 commits

16 changed files with 407 additions and 186 deletions

View file

@ -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'

View file

@ -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...")

View file

@ -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)
})

View file

@ -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

View file

@ -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

View 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)
})

View file

@ -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") {

View file

@ -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 */

View file

@ -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")
})

View file

@ -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",
}])
})

View file

@ -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
} }

View file

@ -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
View file

@ -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
} }
} }

View file

@ -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: {

View file

@ -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),

View file

@ -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")
})() })()