Add /ping command
This commit is contained in:
@@ -227,8 +227,8 @@ async function editToChanges(message, guild, api) {
|
|||||||
*/
|
*/
|
||||||
function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) {
|
function makeReplacementEventContent(oldID, newFallbackContent, newInnerContent) {
|
||||||
const content = {
|
const content = {
|
||||||
...newFallbackContent,
|
|
||||||
"m.mentions": {},
|
"m.mentions": {},
|
||||||
|
...newFallbackContent,
|
||||||
"m.new_content": {
|
"m.new_content": {
|
||||||
...newInnerContent
|
...newInnerContent
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const assert = require("assert").strict
|
||||||
|
const Ty = require("../../types")
|
||||||
|
const DiscordTypes = require("discord-api-types/v10")
|
||||||
|
const {discord, sync, select, from} = require("../../passthrough")
|
||||||
|
const {id: botID} = require("../../../addbot")
|
||||||
|
const {InteractionMethods} = require("snowtransfer")
|
||||||
|
|
||||||
|
/** @type {import("../../matrix/api")} */
|
||||||
|
const api = sync.require("../../matrix/api")
|
||||||
|
/** @type {import("../../matrix/utils")} */
|
||||||
|
const utils = sync.require("../../matrix/utils")
|
||||||
|
/** @type {import("../../web/routes/guild")} */
|
||||||
|
const webGuild = sync.require("../../web/routes/guild")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction
|
||||||
|
* @param {{api: typeof api}} di
|
||||||
|
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
||||||
|
*/
|
||||||
|
async function* _interactAutocomplete({data, channel}, {api}) {
|
||||||
|
function exit() {
|
||||||
|
return {createInteractionResponse: {
|
||||||
|
/** @type {DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult} */
|
||||||
|
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
||||||
|
data: {
|
||||||
|
choices: []
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check it was used in a bridged channel
|
||||||
|
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||||
|
if (!roomID) return yield exit()
|
||||||
|
|
||||||
|
// Check we are in fact autocompleting the first option, the user
|
||||||
|
if (!data.options?.[0] || data.options[0].type !== DiscordTypes.ApplicationCommandOptionType.String || !data.options[0].focused) {
|
||||||
|
return yield exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {{displayname: string | null, mxid: string}[][]} */
|
||||||
|
const providedMatches = []
|
||||||
|
|
||||||
|
const input = data.options[0].value
|
||||||
|
if (input === "") {
|
||||||
|
const events = await api.getEvents(roomID, "b", {limit: 40})
|
||||||
|
const recents = new Set(events.chunk.map(e => e.sender))
|
||||||
|
const matches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL LIMIT 25").all()
|
||||||
|
matches.sort((a, b) => +recents.has(b.mxid) - +recents.has(a.mxid))
|
||||||
|
providedMatches.push(matches)
|
||||||
|
} else if (input.startsWith("@")) { // only autocomplete mxids
|
||||||
|
const query = input.replaceAll(/[%_$]/g, char => `$${char}`) + "%"
|
||||||
|
const matches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND mxid LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
||||||
|
providedMatches.push(matches)
|
||||||
|
} else {
|
||||||
|
const query = "%" + input.replaceAll(/[%_$]/g, char => `$${char}`) + "%"
|
||||||
|
const displaynameMatches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL AND displayname LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
||||||
|
// prioritise matches closer to the start
|
||||||
|
displaynameMatches.sort((a, b) => {
|
||||||
|
let ai = a.displayname.toLowerCase().indexOf(input.toLowerCase())
|
||||||
|
if (ai === -1) ai = 999
|
||||||
|
let bi = b.displayname.toLowerCase().indexOf(input.toLowerCase())
|
||||||
|
if (bi === -1) bi = 999
|
||||||
|
return ai - bi
|
||||||
|
})
|
||||||
|
providedMatches.push(displaynameMatches)
|
||||||
|
let mxidMatches = select("member_cache", ["mxid", "displayname"], {room_id: roomID}, "AND displayname IS NOT NULL AND mxid LIKE ? ESCAPE '$' LIMIT 25").all(query)
|
||||||
|
mxidMatches = mxidMatches.filter(match => {
|
||||||
|
// don't include matches in domain part of mxid
|
||||||
|
if (!match.mxid.match(/^[^:]*/)?.includes(query)) return false
|
||||||
|
if (displaynameMatches.some(m => m.mxid === match.mxid)) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
providedMatches.push(mxidMatches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge together
|
||||||
|
let matches = providedMatches.flat()
|
||||||
|
|
||||||
|
// don't include bot
|
||||||
|
matches = matches.filter(m => m.mxid !== utils.bot)
|
||||||
|
|
||||||
|
// remove duplicates and count up to 25
|
||||||
|
const limitedMatches = []
|
||||||
|
const seen = new Set()
|
||||||
|
for (const match of matches) {
|
||||||
|
if (limitedMatches.length >= 25) break
|
||||||
|
if (seen.has(match.mxid)) continue
|
||||||
|
limitedMatches.push(match)
|
||||||
|
seen.add(match.mxid)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {createInteractionResponse: {
|
||||||
|
type: DiscordTypes.InteractionResponseType.ApplicationCommandAutocompleteResult,
|
||||||
|
data: {
|
||||||
|
choices: limitedMatches.map(row => ({name: (row.displayname || row.mxid).slice(0, 100), value: row.mxid.slice(0, 100)}))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}} interaction
|
||||||
|
* @param {{api: typeof api}} di
|
||||||
|
* @returns {AsyncGenerator<{[k in keyof InteractionMethods]?: Parameters<InteractionMethods[k]>[2]}>}
|
||||||
|
*/
|
||||||
|
async function* _interactCommand({data, channel, guild_id}, {api}) {
|
||||||
|
const roomID = select("channel_room", "room_id", {channel_id: channel.id}).pluck().get()
|
||||||
|
if (!roomID) {
|
||||||
|
return yield {createInteractionResponse: {
|
||||||
|
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||||
|
data: {
|
||||||
|
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||||
|
content: "This channel isn't bridged to Matrix."
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(data.options?.[0]?.type === DiscordTypes.ApplicationCommandOptionType.String)
|
||||||
|
const mxid = data.options[0].value
|
||||||
|
if (!mxid.match(/^@[^:]*:./)) {
|
||||||
|
return yield {createInteractionResponse: {
|
||||||
|
type: DiscordTypes.InteractionResponseType.ChannelMessageWithSource,
|
||||||
|
data: {
|
||||||
|
flags: DiscordTypes.MessageFlags.Ephemeral,
|
||||||
|
content: "⚠️ To use `/ping`, you must select an option from autocomplete, or type a full Matrix ID.\n> Tip: This command is not necessary. You can also ping Matrix users just by typing @their name in your message. It won't look like anything, but it does go through."
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {createInteractionResponse: {
|
||||||
|
type: DiscordTypes.InteractionResponseType.DeferredChannelMessageWithSource
|
||||||
|
}}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @type {Ty.Event.M_Room_Member} */
|
||||||
|
var member = await api.getStateEvent(roomID, "m.room.member", mxid)
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (!member || member.membership !== "join") {
|
||||||
|
const inChannels = discord.guildChannelMap.get(guild_id)
|
||||||
|
.map(cid => discord.channels.get(cid))
|
||||||
|
.sort((a, b) => webGuild._getPosition(a, discord.channels) - webGuild._getPosition(b, discord.channels))
|
||||||
|
.filter(channel => from("channel_room").join("member_cache", "room_id").select("mxid").where({channel_id: channel.id, mxid}).get())
|
||||||
|
if (inChannels.length) {
|
||||||
|
return yield {editOriginalInteractionResponse: {
|
||||||
|
content: `That person isn't in this channel. They have only joined the following channels:\n${inChannels.map(c => `<#${c.id}>`).join(" • ")}\nYou can ask them to join this channel with \`/invite\`.`,
|
||||||
|
}}
|
||||||
|
} else {
|
||||||
|
return yield {editOriginalInteractionResponse: {
|
||||||
|
content: "That person isn't in this channel. You can invite them with `/invite`."
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {editOriginalInteractionResponse: {
|
||||||
|
content: "@" + (member.displayname || mxid)
|
||||||
|
}}
|
||||||
|
|
||||||
|
yield {createFollowupMessage: {
|
||||||
|
flags: DiscordTypes.MessageFlags.Ephemeral | DiscordTypes.MessageFlags.IsComponentsV2,
|
||||||
|
components: [{
|
||||||
|
type: DiscordTypes.ComponentType.Container,
|
||||||
|
components: [{
|
||||||
|
type: DiscordTypes.ComponentType.TextDisplay,
|
||||||
|
content: "Tip: This command is not necessary. You can also ping Matrix users just by typing @their name in your message. It won't look like anything, but it does go through."
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* c8 ignore start */
|
||||||
|
|
||||||
|
/** @param {(DiscordTypes.APIChatInputApplicationCommandGuildInteraction & {channel: DiscordTypes.APIGuildTextChannel}) | DiscordTypes.APIApplicationCommandAutocompleteGuildInteraction} interaction */
|
||||||
|
async function interact(interaction) {
|
||||||
|
if (interaction.type === DiscordTypes.InteractionType.ApplicationCommandAutocomplete) {
|
||||||
|
for await (const response of _interactAutocomplete(interaction, {api})) {
|
||||||
|
if (response.createInteractionResponse) {
|
||||||
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for await (const response of _interactCommand(interaction, {api})) {
|
||||||
|
if (response.createInteractionResponse) {
|
||||||
|
await discord.snow.interaction.createInteractionResponse(interaction.id, interaction.token, response.createInteractionResponse)
|
||||||
|
} else if (response.editOriginalInteractionResponse) {
|
||||||
|
await discord.snow.interaction.editOriginalInteractionResponse(botID, interaction.token, response.editOriginalInteractionResponse)
|
||||||
|
} else if (response.createFollowupMessage) {
|
||||||
|
await discord.snow.interaction.createFollowupMessage(botID, interaction.token, response.createFollowupMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.interact = interact
|
||||||
@@ -10,6 +10,7 @@ const permissions = sync.require("./interactions/permissions.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")
|
||||||
const poll = sync.require("./interactions/poll.js")
|
const poll = sync.require("./interactions/poll.js")
|
||||||
|
const ping = sync.require("./interactions/ping.js")
|
||||||
|
|
||||||
// User must have EVERY permission in default_member_permissions to be able to use the command
|
// User must have EVERY permission in default_member_permissions to be able to use the command
|
||||||
|
|
||||||
@@ -38,6 +39,20 @@ discord.snow.interaction.bulkOverwriteApplicationCommands(id, [{
|
|||||||
description: "The Matrix user to invite, e.g. @username:example.org",
|
description: "The Matrix user to invite, e.g. @username:example.org",
|
||||||
name: "user"
|
name: "user"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
name: "ping",
|
||||||
|
contexts: [DiscordTypes.InteractionContextType.Guild],
|
||||||
|
type: DiscordTypes.ApplicationCommandType.ChatInput,
|
||||||
|
description: "Ping a Matrix user.",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
type: DiscordTypes.ApplicationCommandOptionType.String,
|
||||||
|
description: "Display name or ID of the Matrix user",
|
||||||
|
name: "user",
|
||||||
|
autocomplete: true,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
name: "privacy",
|
name: "privacy",
|
||||||
@@ -94,6 +109,8 @@ async function dispatchInteraction(interaction) {
|
|||||||
await permissions.interactEdit(interaction)
|
await permissions.interactEdit(interaction)
|
||||||
} else if (interactionId === "Reactions") {
|
} else if (interactionId === "Reactions") {
|
||||||
await reactions.interact(interaction)
|
await reactions.interact(interaction)
|
||||||
|
} else if (interactionId === "ping") {
|
||||||
|
await ping.interact(interaction)
|
||||||
} else if (interactionId === "privacy") {
|
} else if (interactionId === "privacy") {
|
||||||
await privacy.interact(interaction)
|
await privacy.interact(interaction)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -128,6 +128,19 @@ async function getEventForTimestamp(roomID, ts) {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} roomID
|
||||||
|
* @param {"b" | "f"} dir
|
||||||
|
* @param {{from?: string, limit?: any}} [pagination]
|
||||||
|
* @param {any} [filter]
|
||||||
|
*/
|
||||||
|
async function getEvents(roomID, dir, pagination = {}, filter) {
|
||||||
|
filter = filter && JSON.stringify(filter)
|
||||||
|
/** @type {Ty.Pagination<Ty.Event.Outer<any>>} */
|
||||||
|
const root = await mreq.mreq("GET", path(`/client/v3/rooms/${roomID}/messages`, null, {...pagination, dir, filter}))
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} roomID
|
* @param {string} roomID
|
||||||
* @returns {Promise<Ty.Event.StateOuter<any>[]>}
|
* @returns {Promise<Ty.Event.StateOuter<any>[]>}
|
||||||
@@ -583,6 +596,7 @@ module.exports.leaveRoom = leaveRoom
|
|||||||
module.exports.leaveRoomWithReason = leaveRoomWithReason
|
module.exports.leaveRoomWithReason = leaveRoomWithReason
|
||||||
module.exports.getEvent = getEvent
|
module.exports.getEvent = getEvent
|
||||||
module.exports.getEventForTimestamp = getEventForTimestamp
|
module.exports.getEventForTimestamp = getEventForTimestamp
|
||||||
|
module.exports.getEvents = getEvents
|
||||||
module.exports.getAllState = getAllState
|
module.exports.getAllState = getAllState
|
||||||
module.exports.getStateEvent = getStateEvent
|
module.exports.getStateEvent = getStateEvent
|
||||||
module.exports.getStateEventOuter = getStateEventOuter
|
module.exports.getStateEventOuter = getStateEventOuter
|
||||||
|
|||||||
Reference in New Issue
Block a user