mirror of
https://gitdab.com/cadence/out-of-your-element.git
synced 2025-09-11 04:33:02 +02:00
Move everything to src folder... it had to happen
This commit is contained in:
parent
decc32f7e6
commit
4247a3114a
103 changed files with 1 additions and 1 deletions
274
src/discord/discord-command-handler.js
Normal file
274
src/discord/discord-command-handler.js
Normal file
|
@ -0,0 +1,274 @@
|
|||
// @ts-check
|
||||
|
||||
const assert = require("assert").strict
|
||||
const util = require("util")
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {reg} = require("../matrix/read-registration")
|
||||
const {addbot} = require("../addbot")
|
||||
|
||||
const {discord, sync, db, select} = require("../passthrough")
|
||||
/** @type {import("../matrix/api")}) */
|
||||
const api = sync.require("../matrix/api")
|
||||
/** @type {import("../matrix/file")} */
|
||||
const file = sync.require("../matrix/file")
|
||||
/** @type {import("../m2d/converters/utils")} */
|
||||
const mxUtils = sync.require("../matrix/utils")
|
||||
/** @type {import("../d2m/actions/create-space")} */
|
||||
const createSpace = sync.require("../d2m/actions/create-space")
|
||||
/** @type {import("./utils")} */
|
||||
const utils = sync.require("./utils")
|
||||
|
||||
const PREFIX = "//"
|
||||
|
||||
let buttons = []
|
||||
|
||||
/**
|
||||
* @param {string} channelID where to add the button
|
||||
* @param {string} messageID where to add the button
|
||||
* @param {string} emoji emoji to add as a button
|
||||
* @param {string} userID only listen for responses from this user
|
||||
* @returns {Promise<import("discord-api-types/v10").GatewayMessageReactionAddDispatchData>}
|
||||
*/
|
||||
async function addButton(channelID, messageID, emoji, userID) {
|
||||
await discord.snow.channel.createReaction(channelID, messageID, emoji)
|
||||
return new Promise(resolve => {
|
||||
buttons.push({channelID, messageID, userID, resolve, created: Date.now()})
|
||||
})
|
||||
}
|
||||
|
||||
// Clear out old buttons every so often to free memory
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
buttons = buttons.filter(b => now - b.created < 2*60*60*1000)
|
||||
}, 10*60*1000)
|
||||
|
||||
/** @param {import("discord-api-types/v10").GatewayMessageReactionAddDispatchData} data */
|
||||
function onReactionAdd(data) {
|
||||
const button = buttons.find(b => b.channelID === data.channel_id && b.messageID === data.message_id && b.userID === data.user_id)
|
||||
if (button) {
|
||||
buttons = buttons.filter(b => b !== button) // remove button data so it can't be clicked again
|
||||
button.resolve(data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback CommandExecute
|
||||
* @param {DiscordTypes.GatewayMessageCreateDispatchData} message
|
||||
* @param {DiscordTypes.APIGuildTextChannel} channel
|
||||
* @param {DiscordTypes.APIGuild} guild
|
||||
* @param {Partial<DiscordTypes.RESTPostAPIChannelMessageJSONBody>} [ctx]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Command
|
||||
* @property {string[]} aliases
|
||||
* @property {(message: DiscordTypes.GatewayMessageCreateDispatchData, channel: DiscordTypes.APIGuildTextChannel, guild: DiscordTypes.APIGuild) => Promise<any>} execute
|
||||
*/
|
||||
|
||||
/** @param {CommandExecute} execute */
|
||||
function replyctx(execute) {
|
||||
/** @type {CommandExecute} */
|
||||
return function(message, channel, guild, ctx = {}) {
|
||||
ctx.message_reference = {
|
||||
message_id: message.id,
|
||||
channel_id: channel.id,
|
||||
guild_id: guild.id,
|
||||
fail_if_not_exists: false
|
||||
}
|
||||
return execute(message, channel, guild, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Command[]} */
|
||||
const commands = [{
|
||||
aliases: ["icon", "avatar", "roomicon", "roomavatar", "channelicon", "channelavatar"],
|
||||
execute: replyctx(
|
||||
async (message, channel, guild, ctx) => {
|
||||
// Guard
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
if (!roomID) return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "This channel isn't bridged to the other side."
|
||||
})
|
||||
|
||||
// Current avatar
|
||||
const avatarEvent = await api.getStateEvent(roomID, "m.room.avatar", "")
|
||||
let currentAvatarMessage =
|
||||
( avatarEvent.url ? `Current room-specific avatar: ${mxUtils.getPublicUrlForMxc(avatarEvent.url)}`
|
||||
: "No avatar. Now's your time to strike. Use `//icon` again with a link or upload to set the room-specific avatar.")
|
||||
|
||||
// Next potential avatar
|
||||
const nextAvatarURL = message.attachments.find(a => a.content_type?.startsWith("image/"))?.url || message.content.match(/https?:\/\/[^ ]+\.[^ ]+\.(?:png|jpg|jpeg|webp)\b/)?.[0]
|
||||
let nextAvatarMessage =
|
||||
( nextAvatarURL ? `\nYou want to set it to: ${nextAvatarURL}\nHit ✅ to make it happen.`
|
||||
: "")
|
||||
|
||||
const sent = await discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: currentAvatarMessage + nextAvatarMessage
|
||||
})
|
||||
|
||||
if (nextAvatarURL) {
|
||||
addButton(channel.id, sent.id, "✅", message.author.id).then(async data => {
|
||||
const mxcUrl = await file.uploadDiscordFileToMxc(nextAvatarURL)
|
||||
await api.sendState(roomID, "m.room.avatar", "", {
|
||||
url: mxcUrl
|
||||
})
|
||||
db.prepare("UPDATE channel_room SET custom_avatar = ? WHERE channel_id = ?").run(mxcUrl, channel.id)
|
||||
await discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "Your creation is unleashed. Any complaints will be redirected to Grelbo."
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}, {
|
||||
aliases: ["invite"],
|
||||
execute: replyctx(
|
||||
async (message, channel, guild, ctx) => {
|
||||
// Check guild is bridged
|
||||
const spaceID = select("guild_space", "space_id", {guild_id: guild.id}).pluck().get()
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
if (!spaceID || !roomID) return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "This server isn't bridged to Matrix, so you can't invite Matrix users."
|
||||
})
|
||||
|
||||
// Check CREATE_INSTANT_INVITE permission
|
||||
assert(message.member)
|
||||
const guildPermissions = utils.getPermissions(message.member.roles, guild.roles)
|
||||
if (!(guildPermissions & DiscordTypes.PermissionFlagsBits.CreateInstantInvite)) {
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "You don't have permission to invite people to this Discord server."
|
||||
})
|
||||
}
|
||||
|
||||
// Guard against accidental mentions instead of the MXID
|
||||
if (message.content.match(/<[@#:].*>/)) return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "You have to say the Matrix ID of the person you want to invite, but you mentioned a Discord user in your message.\nOne way to fix this is by writing `` ` `` backticks `` ` `` around the Matrix ID."
|
||||
})
|
||||
|
||||
// Get named MXID
|
||||
const mxid = message.content.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0]
|
||||
if (!mxid) return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`"
|
||||
})
|
||||
|
||||
// Check for existing invite to the space
|
||||
let spaceMember
|
||||
try {
|
||||
spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
if (spaceMember && spaceMember.membership === "invite") {
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`
|
||||
})
|
||||
}
|
||||
|
||||
// Invite Matrix user if not in space
|
||||
if (!spaceMember || spaceMember.membership !== "join") {
|
||||
await api.inviteToRoom(spaceID, mxid)
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `You invited \`${mxid}\` to the server.`
|
||||
})
|
||||
}
|
||||
|
||||
// The Matrix user *is* in the space, maybe we want to invite them to this channel?
|
||||
let roomMember
|
||||
try {
|
||||
roomMember = await api.getStateEvent(roomID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) {
|
||||
const sent = await discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?\nHit ✅ to make it happen.`
|
||||
})
|
||||
return addButton(channel.id, sent.id, "✅", message.author.id).then(async data => {
|
||||
await api.inviteToRoom(roomID, mxid)
|
||||
await discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `You invited \`${mxid}\` to the channel.`
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// The Matrix user *is* in the space and in the channel.
|
||||
await discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `\`${mxid}\` is already in this server and this channel.`
|
||||
})
|
||||
}
|
||||
)
|
||||
}, {
|
||||
aliases: ["addbot"],
|
||||
execute: replyctx(
|
||||
async (message, channel, guild, ctx) => {
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: addbot()
|
||||
})
|
||||
}
|
||||
)
|
||||
}, {
|
||||
aliases: ["privacy", "discoverable", "publish", "published"],
|
||||
execute: replyctx(
|
||||
async (message, channel, guild, ctx) => {
|
||||
const current = select("guild_space", "privacy_level", {guild_id: guild.id}).pluck().get()
|
||||
if (current == null) {
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "This server isn't bridged to the other side."
|
||||
})
|
||||
}
|
||||
|
||||
const levels = ["invite", "link", "directory"]
|
||||
const level = levels.findIndex(x => message.content.includes(x))
|
||||
if (level === -1) {
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "**Usage: `//privacy <level>`**. This will set who can join the space on Matrix-side. There are three levels:"
|
||||
+ "\n`invite`: Can only join with a direct in-app invite from another Matrix user, or the //invite command."
|
||||
+ "\n`link`: Matrix links can be created and shared like Discord's invite links. `invite` features also work."
|
||||
+ "\n`directory`: Publishes to the Matrix in-app directory, like Server Discovery. Preview enabled. `invite` and `link` also work."
|
||||
+ `\n**Current privacy level: \`${levels[current]}\`**`
|
||||
})
|
||||
}
|
||||
|
||||
assert(message.member)
|
||||
const guildPermissions = utils.getPermissions(message.member.roles, guild.roles)
|
||||
if (guild.owner_id !== message.author.id && !(guildPermissions & BigInt(0x28))) { // MANAGE_GUILD | ADMINISTRATOR
|
||||
return discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: "You don't have permission to change the privacy level. You need Manage Server or Administrator."
|
||||
})
|
||||
}
|
||||
|
||||
db.prepare("UPDATE guild_space SET privacy_level = ? WHERE guild_id = ?").run(level, guild.id)
|
||||
discord.snow.channel.createMessage(channel.id, {
|
||||
...ctx,
|
||||
content: `Privacy level updated to \`${levels[level]}\`. Changes will apply shortly.`
|
||||
})
|
||||
await createSpace.syncSpaceFully(guild.id)
|
||||
}
|
||||
)
|
||||
}]
|
||||
|
||||
/** @type {CommandExecute} */
|
||||
async function execute(message, channel, guild) {
|
||||
if (!message.content.startsWith(PREFIX)) return
|
||||
const words = message.content.slice(PREFIX.length).split(" ")
|
||||
const commandName = words[0]
|
||||
const command = commands.find(c => c.aliases.includes(commandName))
|
||||
if (!command) return
|
||||
|
||||
await command.execute(message, channel, guild)
|
||||
}
|
||||
|
||||
module.exports.execute = execute
|
||||
module.exports.onReactionAdd = onReactionAdd
|
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
|
131
src/discord/interactions/invite.js
Normal file
131
src/discord/interactions/invite.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const assert = require("assert/strict")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction
|
||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||
*/
|
||||
async function _interact({data, channel, guild_id}) {
|
||||
// Check guild is bridged
|
||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
if (!spaceID || !roomID) return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "This server isn't bridged to Matrix, so you can't invite Matrix users.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
|
||||
// Get named MXID
|
||||
/** @type {DiscordTypes.APIApplicationCommandInteractionDataStringOption[] | undefined} */ // @ts-ignore
|
||||
const options = data.options
|
||||
const input = options?.[0].value || ""
|
||||
const mxid = input.match(/@([^:]+):([a-z0-9:-]+\.[a-z0-9.:-]+)/)?.[0]
|
||||
if (!mxid) return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "You have to say the Matrix ID of the person you want to invite. Matrix IDs look like this: `@username:example.org`",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
|
||||
// Check for existing invite to the space
|
||||
let spaceMember
|
||||
try {
|
||||
spaceMember = await api.getStateEvent(spaceID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
if (spaceMember && spaceMember.membership === "invite") {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${mxid}\` already has an invite, which they haven't accepted yet.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invite Matrix user if not in space
|
||||
if (!spaceMember || spaceMember.membership !== "join") {
|
||||
await api.inviteToRoom(spaceID, mxid)
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `You invited \`${mxid}\` to the server.`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Matrix user *is* in the space, maybe we want to invite them to this channel?
|
||||
let roomMember
|
||||
try {
|
||||
roomMember = await api.getStateEvent(roomID, "m.room.member", mxid)
|
||||
} catch (e) {}
|
||||
if (!roomMember || (roomMember.membership !== "join" && roomMember.membership !== "invite")) {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${mxid}\` is already in this server. Would you like to additionally invite them to this specific channel?`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.ActionRow,
|
||||
components: [{
|
||||
type: DiscordTypes.ComponentType.Button,
|
||||
custom_id: "invite_channel",
|
||||
style: DiscordTypes.ButtonStyle.Primary,
|
||||
label: "Sure",
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Matrix user *is* in the space and in the channel.
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${mxid}\` is already in this server and this channel.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction
|
||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||
*/
|
||||
async function _interactButton({channel, message}) {
|
||||
const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1]
|
||||
assert(mxid)
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
await api.inviteToRoom(roomID, mxid)
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.UpdateMessage,
|
||||
data: {
|
||||
content: `You invited \`${mxid}\` to the channel.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||
components: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction} interaction */
|
||||
async function interact(interaction) {
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
||||
}
|
||||
|
||||
/** @param {DiscordTypes.APIMessageComponentGuildInteraction} interaction */
|
||||
async function interactButton(interaction) {
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interactButton(interaction))
|
||||
}
|
||||
|
||||
module.exports.interact = interact
|
||||
module.exports.interactButton = interactButton
|
||||
module.exports._interact = _interact
|
||||
module.exports._interactButton = _interactButton
|
51
src/discord/interactions/matrix-info.js
Normal file
51
src/discord/interactions/matrix-info.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
|
||||
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
|
||||
/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */
|
||||
async function interact({id, token, guild_id, channel, data}) {
|
||||
const message = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id")
|
||||
.select("name", "nick", "source", "room_id", "event_id").where({message_id: data.target_id}).get()
|
||||
|
||||
if (!message) {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "This message hasn't been bridged to Matrix.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const idInfo = `\n-# Room ID: \`${message.room_id}\`\n-# Event ID: \`${message.event_id}\``
|
||||
|
||||
if (message.source === 1) { // from Discord
|
||||
const userID = data.resolved.messages[data.target_id].author.id
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `Bridged <@${userID}> https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord to [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix.`
|
||||
+ idInfo,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// from Matrix
|
||||
const event = await api.getEvent(message.room_id, message.event_id)
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `Bridged [${event.sender}](<https://matrix.to/#/${event.sender}>)'s message in [${message.nick || message.name}](<https://matrix.to/#/${message.room_id}/${message.event_id}>) on Matrix to https://discord.com/channels/${guild_id}/${channel.id}/${data.target_id} on Discord.`
|
||||
+ idInfo,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.interact = interact
|
129
src/discord/interactions/permissions.js
Normal file
129
src/discord/interactions/permissions.js
Normal file
|
@ -0,0 +1,129 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const Ty = require("../../types")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
const assert = require("assert/strict")
|
||||
|
||||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIContextMenuGuildInteraction} interaction
|
||||
* @returns {Promise<DiscordTypes.APIInteractionResponse>}
|
||||
*/
|
||||
async function _interact({data, channel, guild_id}) {
|
||||
const row = select("event_message", ["event_id", "source"], {message_id: data.target_id}).get()
|
||||
assert(row)
|
||||
|
||||
// Can't operate on Discord users
|
||||
if (row.source === 1) { // discord
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `This command is only meaningful for Matrix users.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the message sender, the person that will be inspected/edited
|
||||
const eventID = row.event_id
|
||||
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||
assert(roomID)
|
||||
const event = await api.getEvent(roomID, eventID)
|
||||
const sender = event.sender
|
||||
|
||||
// Get the space, where the power levels will be inspected/edited
|
||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
||||
assert(spaceID)
|
||||
|
||||
// Get the power level
|
||||
/** @type {Ty.Event.M_Power_Levels} */
|
||||
const powerLevelsContent = await api.getStateEvent(spaceID, "m.room.power_levels", "")
|
||||
const userPower = powerLevelsContent.users?.[event.sender] || 0
|
||||
|
||||
// Administrators equal to the bot cannot be demoted
|
||||
if (userPower >= 100) {
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `\`${sender}\` has administrator permissions. This cannot be edited.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `Showing permissions for \`${sender}\`. Click to edit.`,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||
components: [
|
||||
{
|
||||
type: DiscordTypes.ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: DiscordTypes.ComponentType.StringSelect,
|
||||
custom_id: "permissions_edit",
|
||||
options: [
|
||||
{
|
||||
label: "Default",
|
||||
value: "default",
|
||||
default: userPower < 50
|
||||
}, {
|
||||
label: "Moderator",
|
||||
value: "moderator",
|
||||
default: userPower >= 50 && userPower < 100
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DiscordTypes.APIMessageComponentSelectMenuInteraction} interaction
|
||||
*/
|
||||
async function interactEdit({data, id, token, guild_id, message}) {
|
||||
// Get the person that will be inspected/edited
|
||||
const mxid = message.content.match(/`(@(?:[^:]+):(?:[a-z0-9:-]+\.[a-z0-9.:-]+))`/)?.[1]
|
||||
assert(mxid)
|
||||
|
||||
const permission = data.values[0]
|
||||
const power = permission === "moderator" ? 50 : 0
|
||||
|
||||
await discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.UpdateMessage,
|
||||
data: {
|
||||
content: `Updating \`${mxid}\` to **${permission}**, please wait...`,
|
||||
components: []
|
||||
}
|
||||
})
|
||||
|
||||
// Get the space, where the power levels will be inspected/edited
|
||||
const spaceID = select("guild_space", "space_id", {guild_id}).pluck().get()
|
||||
assert(spaceID)
|
||||
|
||||
// Do it
|
||||
await api.setUserPowerCascade(spaceID, mxid, power)
|
||||
|
||||
// ACK
|
||||
await discord.snow.interaction.editOriginalInteractionResponse(discord.application.id, token, {
|
||||
content: `Updated \`${mxid}\` to **${permission}**.`,
|
||||
components: []
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
|
||||
async function interact(interaction) {
|
||||
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, await _interact(interaction))
|
||||
}
|
||||
|
||||
module.exports.interact = interact
|
||||
module.exports.interactEdit = interactEdit
|
||||
module.exports._interact = _interact
|
58
src/discord/interactions/reactions.js
Normal file
58
src/discord/interactions/reactions.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {discord, sync, db, select, from} = require("../../passthrough")
|
||||
|
||||
/** @type {import("../../matrix/api")} */
|
||||
const api = sync.require("../../matrix/api")
|
||||
/** @type {import("../../m2d/converters/utils")} */
|
||||
const utils = sync.require("../../m2d/converters/utils")
|
||||
|
||||
/** @param {DiscordTypes.APIContextMenuGuildInteraction} interaction */
|
||||
/** @param {DiscordTypes.APIMessageApplicationCommandGuildInteraction} interaction */
|
||||
async function interact({id, token, data}) {
|
||||
const row = from("event_message").join("message_channel", "message_id").join("channel_room", "channel_id")
|
||||
.select("event_id", "room_id").where({message_id: data.target_id}).get()
|
||||
if (!row) {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "This message hasn't been bridged to Matrix.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reactions = await api.getFullRelations(row.room_id, row.event_id, "m.annotation")
|
||||
|
||||
/** @type {Map<string, string[]>} */
|
||||
const inverted = new Map()
|
||||
for (const reaction of reactions) {
|
||||
if (utils.eventSenderIsFromDiscord(reaction.sender)) continue
|
||||
const key = reaction.content["m.relates_to"].key
|
||||
const displayname = select("member_cache", "displayname", {mxid: reaction.sender, room_id: row.room_id}).pluck().get() || reaction.sender
|
||||
if (!inverted.has(key)) inverted.set(key, [])
|
||||
// @ts-ignore
|
||||
inverted.get(key).push(displayname)
|
||||
}
|
||||
|
||||
if (inverted.size === 0) {
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: "Nobody from Matrix reacted to this message.",
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return discord.snow.interaction.createInteractionResponse(id, token, {
|
||||
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: [...inverted.entries()].map(([key, value]) => `${key} ⮞ ${value.join(" ⬩ ")}`).join("\n"),
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.interact = interact
|
94
src/discord/register-interactions.js
Normal file
94
src/discord/register-interactions.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {discord, sync, db, select} = require("../passthrough")
|
||||
const {id} = require("../addbot")
|
||||
|
||||
const matrixInfo = sync.require("./interactions/matrix-info.js")
|
||||
const invite = sync.require("./interactions/invite.js")
|
||||
const permissions = sync.require("./interactions/permissions.js")
|
||||
const bridge = sync.require("./interactions/bridge.js")
|
||||
const reactions = sync.require("./interactions/reactions.js")
|
||||
|
||||
discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
||||
name: "Matrix info",
|
||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||
type: DiscordTypes.ApplicationCommandType.Message,
|
||||
}, {
|
||||
name: "Permissions",
|
||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||
type: DiscordTypes.ApplicationCommandType.Message,
|
||||
default_member_permissions: String(DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.ManageRoles)
|
||||
}, {
|
||||
name: "Reactions",
|
||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||
type: DiscordTypes.ApplicationCommandType.Message
|
||||
}, {
|
||||
name: "invite",
|
||||
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||
type: DiscordTypes.ApplicationCommandType.ChatInput,
|
||||
description: "Invite a Matrix user to this Discord server",
|
||||
default_member_permissions: String(DiscordTypes.PermissionFlagsBits.CreateInstantInvite),
|
||||
options: [
|
||||
{
|
||||
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||
description: "The Matrix user to invite, e.g. @username:example.org",
|
||||
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
|
||||
}
|
||||
]
|
||||
}])
|
||||
|
||||
async function dispatchInteraction(interaction) {
|
||||
const interactionId = interaction.data.custom_id || interaction.data.name
|
||||
try {
|
||||
console.log(interaction)
|
||||
if (interactionId === "Matrix info") {
|
||||
await matrixInfo.interact(interaction)
|
||||
} else if (interactionId === "invite") {
|
||||
await invite.interact(interaction)
|
||||
} else if (interactionId === "invite_channel") {
|
||||
await invite.interactButton(interaction)
|
||||
} else if (interactionId === "Permissions") {
|
||||
await permissions.interact(interaction)
|
||||
} else if (interactionId === "permissions_edit") {
|
||||
await permissions.interactEdit(interaction)
|
||||
} else if (interactionId === "bridge") {
|
||||
await bridge.interact(interaction)
|
||||
} else if (interactionId === "Reactions") {
|
||||
await reactions.interact(interaction)
|
||||
} else {
|
||||
throw new Error(`Unknown interaction ${interactionId}`)
|
||||
}
|
||||
} catch (e) {
|
||||
let stackLines = null
|
||||
if (e.stack) {
|
||||
stackLines = e.stack.split("\n")
|
||||
let cloudstormLine = stackLines.findIndex(l => l.includes("/node_modules/cloudstorm/"))
|
||||
if (cloudstormLine !== -1) {
|
||||
stackLines = stackLines.slice(0, cloudstormLine - 2)
|
||||
}
|
||||
}
|
||||
await discord.snow.interaction.createFollowupMessage(id, interaction.token, {
|
||||
content: `Interaction failed: **${interactionId}**`
|
||||
+ `\nError trace:\n\`\`\`\n${stackLines.join("\n")}\`\`\``
|
||||
+ `Interaction data:\n\`\`\`\n${JSON.stringify(interaction.data, null, 2)}\`\`\``,
|
||||
flags: DiscordTypes.MessageFlags.Ephemeral
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.dispatchInteraction = dispatchInteraction
|
127
src/discord/utils.js
Normal file
127
src/discord/utils.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
// @ts-check
|
||||
|
||||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const assert = require("assert").strict
|
||||
|
||||
const EPOCH = 1420070400000
|
||||
|
||||
/**
|
||||
* @param {string[]} userRoles
|
||||
* @param {DiscordTypes.APIGuild["roles"]} guildRoles
|
||||
* @param {string} [userID]
|
||||
* @param {DiscordTypes.APIGuildChannel["permission_overwrites"]} [channelOverwrites]
|
||||
*/
|
||||
function getPermissions(userRoles, guildRoles, userID, channelOverwrites) {
|
||||
let allowed = BigInt(0)
|
||||
let everyoneID
|
||||
// Guild allows
|
||||
for (const role of guildRoles) {
|
||||
if (role.name === "@everyone") {
|
||||
allowed |= BigInt(role.permissions)
|
||||
everyoneID = role.id
|
||||
}
|
||||
if (userRoles.includes(role.id)) {
|
||||
allowed |= BigInt(role.permissions)
|
||||
}
|
||||
}
|
||||
|
||||
if (channelOverwrites) {
|
||||
/** @type {((overwrite: Required<DiscordTypes.APIOverwrite>) => any)[]} */
|
||||
const actions = [
|
||||
// Channel @everyone deny
|
||||
overwrite => overwrite.id === everyoneID && (allowed &= ~BigInt(overwrite.deny)),
|
||||
// Channel @everyone allow
|
||||
overwrite => overwrite.id === everyoneID && (allowed |= BigInt(overwrite.allow)),
|
||||
// Role deny
|
||||
overwrite => userRoles.includes(overwrite.id) && (allowed &= ~BigInt(overwrite.deny)),
|
||||
// Role allow
|
||||
overwrite => userRoles.includes(overwrite.id) && (allowed |= BigInt(overwrite.allow)),
|
||||
// User deny
|
||||
overwrite => overwrite.id === userID && (allowed &= ~BigInt(overwrite.deny)),
|
||||
// User allow
|
||||
overwrite => overwrite.id === userID && (allowed |= BigInt(overwrite.allow))
|
||||
]
|
||||
for (let i = 0; i < actions.length; i++) {
|
||||
for (const overwrite of channelOverwrites) {
|
||||
actions[i](overwrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: You can only provide one permission bit to permissionToCheckFor. To check multiple permissions, call `hasAllPermissions` or `hasSomePermissions`.
|
||||
* It is designed like this to avoid developer error with bit manipulations.
|
||||
*
|
||||
* @param {bigint} resolvedPermissions
|
||||
* @param {bigint} permissionToCheckFor
|
||||
* @returns {boolean} whether the user has the requested permission
|
||||
* @example
|
||||
* const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
|
||||
* hasPermission(permissions, DiscordTypes.PermissionFlagsBits.ViewChannel)
|
||||
*/
|
||||
function hasPermission(resolvedPermissions, permissionToCheckFor) {
|
||||
// Make sure permissionToCheckFor has exactly one permission in it
|
||||
assert.equal(permissionToCheckFor.toString(2).match(/1/g)?.length, 1)
|
||||
// Do the actual calculation
|
||||
return (resolvedPermissions & permissionToCheckFor) === permissionToCheckFor
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} resolvedPermissions
|
||||
* @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor
|
||||
* @returns {boolean} whether the user has any of the requested permissions
|
||||
* @example
|
||||
* const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
|
||||
* hasSomePermissions(permissions, ["ViewChannel", "ReadMessageHistory"])
|
||||
*/
|
||||
function hasSomePermissions(resolvedPermissions, permissionsToCheckFor) {
|
||||
return permissionsToCheckFor.some(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {bigint} resolvedPermissions
|
||||
* @param {(keyof DiscordTypes.PermissionFlagsBits)[]} permissionsToCheckFor
|
||||
* @returns {boolean} whether the user has all of the requested permissions
|
||||
* @example
|
||||
* const permissions = getPermissions(userRoles, guildRoles, userID, channelOverwrites)
|
||||
* hasAllPermissions(permissions, ["ViewChannel", "ReadMessageHistory"])
|
||||
*/
|
||||
function hasAllPermissions(resolvedPermissions, permissionsToCheckFor) {
|
||||
return permissionsToCheckFor.every(x => hasPermission(resolvedPermissions, DiscordTypes.PermissionFlagsBits[x]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Command interaction responses have a webhook_id for some reason, but still have real author info of a real bot user in the server.
|
||||
* @param {DiscordTypes.APIMessage} message
|
||||
*/
|
||||
function isWebhookMessage(message) {
|
||||
return message.webhook_id && message.type !== DiscordTypes.MessageType.ChatInputCommand
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Pick<DiscordTypes.APIMessage, "flags">} message
|
||||
*/
|
||||
function isEphemeralMessage(message) {
|
||||
return message.flags && (message.flags & DiscordTypes.MessageFlags.Ephemeral)
|
||||
}
|
||||
|
||||
/** @param {string} snowflake */
|
||||
function snowflakeToTimestampExact(snowflake) {
|
||||
return Number(BigInt(snowflake) >> 22n) + EPOCH
|
||||
}
|
||||
|
||||
/** @param {number} timestamp */
|
||||
function timestampToSnowflakeInexact(timestamp) {
|
||||
return String((timestamp - EPOCH) * 2**22)
|
||||
}
|
||||
|
||||
module.exports.getPermissions = getPermissions
|
||||
module.exports.hasPermission = hasPermission
|
||||
module.exports.hasSomePermissions = hasSomePermissions
|
||||
module.exports.hasAllPermissions = hasAllPermissions
|
||||
module.exports.isWebhookMessage = isWebhookMessage
|
||||
module.exports.isEphemeralMessage = isEphemeralMessage
|
||||
module.exports.snowflakeToTimestampExact = snowflakeToTimestampExact
|
||||
module.exports.timestampToSnowflakeInexact = timestampToSnowflakeInexact
|
109
src/discord/utils.test.js
Normal file
109
src/discord/utils.test.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
const DiscordTypes = require("discord-api-types/v10")
|
||||
const {test} = require("supertape")
|
||||
const data = require("../test/data")
|
||||
const utils = require("./utils")
|
||||
|
||||
test("is webhook message: identifies bot interaction response as not a message", t => {
|
||||
t.equal(utils.isWebhookMessage(data.interaction_message.thinking_interaction), false)
|
||||
})
|
||||
|
||||
test("is webhook message: identifies webhook interaction response as not a message", t => {
|
||||
t.equal(utils.isWebhookMessage(data.interaction_message.thinking_interaction_without_bot_user), false)
|
||||
})
|
||||
|
||||
test("is webhook message: identifies webhook message as a message", t => {
|
||||
t.equal(utils.isWebhookMessage(data.special_message.bridge_echo_webhook), true)
|
||||
})
|
||||
|
||||
test("discord utils: converts snowflake to timestamp", t => {
|
||||
t.equal(utils.snowflakeToTimestampExact("86913608335773696"), 1440792219004)
|
||||
})
|
||||
|
||||
test("discord utils: converts timestamp to snowflake", t => {
|
||||
t.match(utils.timestampToSnowflakeInexact(1440792219004), /^869136083357.....$/)
|
||||
})
|
||||
|
||||
test("getPermissions: channel overwrite to allow role 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 = [ "1168988246680801360" ]
|
||||
const userID = "684280192553844747"
|
||||
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 => {
|
||||
const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.BanMembers
|
||||
const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||
t.equal(canRemoveMembers, true)
|
||||
})
|
||||
|
||||
test("hasSomePermissions: doesn't detect not the permission", t => {
|
||||
const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages
|
||||
const canRemoveMembers = utils.hasSomePermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||
t.equal(canRemoveMembers, false)
|
||||
})
|
||||
|
||||
test("hasAllPermissions: detects the permissions", t => {
|
||||
const userPermissions = DiscordTypes.PermissionFlagsBits.KickMembers | DiscordTypes.PermissionFlagsBits.BanMembers | DiscordTypes.PermissionFlagsBits.MentionEveryone
|
||||
const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||
t.equal(canRemoveMembers, true)
|
||||
})
|
||||
|
||||
test("hasAllPermissions: doesn't detect not the permissions", t => {
|
||||
const userPermissions = DiscordTypes.PermissionFlagsBits.MentionEveryone | DiscordTypes.PermissionFlagsBits.SendMessages | DiscordTypes.PermissionFlagsBits.KickMembers
|
||||
const canRemoveMembers = utils.hasAllPermissions(userPermissions, ["KickMembers", "BanMembers"])
|
||||
t.equal(canRemoveMembers, false)
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue