Where the interaction happened
Tell a server install from a user install, and a server from a DM, and answer each the way it allows.
You'll learn
- Read where a command was used with getInstallContext
- Know what respond() can do in each of the four places
- Test a command in each place
Before this
An app can be installed to a server, or by a member to their own account. A user-installed app's commands follow the member everywhere: into servers the bot isn't in, and into DMs and group DMs between users, where the bot can't see or post to the channel.
So a command can run in four places, and what the bot can do differs between them.
When to use it
Read where a command ran when the answer should depend on it: keep an answer private in a server the bot isn't in, refuse a server-only feature in a DM, or skip posting to a channel the bot can't reach.
If your app is only installed to servers, every command runs where the bot is present, and you rarely need this.
respond() already answers correctly in all four places without your help.
Example
@Command('stats', CommandType.SLASH)
async stats(interaction: ChatInputCommandInteraction) {
const { where, botInstalled } = getInstallContext(interaction)
// In a server the bot is not in, keep the answer to the user who asked
const flags = where === 'guild' && !botInstalled ? MessageFlags.Ephemeral : undefined
await respond(interaction).send({ content: `Asked in ${where}.`, flags })
}/statsOpen in playgroundgetInstallContext(interaction) reports where the command ran and whether the bot
is there. In a server that only a member's own install reaches, the answer stays private.
How it works
getInstallContext reads the interaction's context and authorizingIntegrationOwners, which Discord sends with
every interaction:
| Where | where | botInstalled |
|---|---|---|
| A server that installed the app | 'guild' | true |
| Another server, through a user install | 'guild' | false |
| A direct message with the bot | 'bot-dm' | true |
| A direct or group message between users | 'private-channel' | false |
botInstalled says whether the bot is present, which decides whether the channel API is reachable. It says nothing
about the bot's permissions in that channel.
What respond() can do in each place
respond() answers through the interaction's own methods, which work in all four places. It turns to the channel
only after the interaction's fifteen-minute token has expired, and only where the bot is present:
- Within fifteen minutes, everywhere: replies, updates, edits and follow-ups.
- After fifteen minutes, where the bot is present: only edits, sent through the channel. An error can be logged but no longer shown privately.
- After fifteen minutes, where it isn't: nothing more can be sent, and the reason is logged.
A token error from fourteen minutes on counts as expired, to allow for a clock running late.
Letting a command run in more places
A builder chooses where its command can be installed and used, with discord.js's setIntegrationTypes and
setContexts. A command installable to a user and usable in DMs between users reaches 'private-channel':
everything it does has to go through the interaction.
Testing
Mock interactions take context and authorizingIntegrationOwners, the map Discord sends, to put a command in each
of the four places:
const at = (context: InteractionContextType, owners?: Partial<Record<ApplicationIntegrationType, string>>) =>
getInstallContext(
createMockInteraction(ChatInputCommandInteraction, { context, authorizingIntegrationOwners: owners }),
)
describe('getInstallContext', () => {
it('tells the four places a command can run apart', () => {
// The bot's own server, installed to the server
expect(
at(InteractionContextType.Guild, { [ApplicationIntegrationType.GuildInstall]: '876543210987654321' }),
).toEqual({
where: 'guild',
botInstalled: true,
})
// A server without the bot, through a user install
expect(
at(InteractionContextType.Guild, { [ApplicationIntegrationType.UserInstall]: '123456789012345678' }),
).toEqual({
where: 'guild',
botInstalled: false,
})
expect(at(InteractionContextType.BotDM)).toEqual({ where: 'bot-dm', botInstalled: true })
expect(at(InteractionContextType.PrivateChannel)).toEqual({ where: 'private-channel', botInstalled: false })
})
})Gotchas
- Posting to the channel where the bot isn't.
interaction.channel.sendfails in a server the bot isn't in and in DMs between users. Answer throughrespond(), or checkbotInstalledfirst. - A long job outliving its token. Past fifteen minutes, only edits reach the member, and only where the bot is present. Send progress early, or DM the result.
botInstalledisn't permission. The bot can be present and still lack Send Messages in that channel.
Next steps
- Answering with respond(): the answers it chooses.
- Context menus: commands that often run in DMs.
- Mocks: setting where a mock interaction ran.