Interactions overview
Interactive features, like commands and buttons, let a member use your application right inside Moymo. When a member engages with one of them, your application receives an interaction: a signed message from Moymo that says who did what, where, with what, and a token to answer with.
This overview names the kinds of interactions and shows how to prepare your application to receive them. How to answer is in Receiving and responding.
Types of interactions
| Type | Value | What the member did |
|---|---|---|
PING | 1 | Nothing: Moymo checking that your address works |
APPLICATION_COMMAND | 2 | Ran a command: a slash command, or a command on a member or a message |
MESSAGE_COMPONENT | 3 | Pressed a button or chose from a menu on one of your messages |
APPLICATION_COMMAND_AUTOCOMPLETE | 4 | Is typing an option that you offer suggestions for |
MODAL_SUBMIT | 5 | Sent a form you opened |
Commands
Application commands are the native way to use an application. A member types / in a text channel and picks from the commands of every application whose bot is in the server, fills in the options (with suggestions and checks), and sends. Commands can also be on a member or on a message, reached from the right-click menu.
Message components
Components are buttons and select menus on the messages your application writes. Pressing one sends you an interaction with the custom_id you gave it.
Modals
A modal is a form that opens in front of the member in answer to a command or a component, with text inputs and select menus. When they send it you get a MODAL_SUBMIT with what they typed.
Preparing for interactions
Moymo sends interactions to an interactions endpoint: an https address on your server.
Setting up an endpoint
Your endpoint has to do three things on every request:
- Verify the signature. Every request carries
X-Signature-Ed25519andX-Signature-Timestamp. The signature is Ed25519 over the timestamp followed by the raw body, and you check it with your application's public key (on the Interactions tab of the portal, as 64 hex characters). Answer401to anything that does not verify. - Answer a ping. A request with
{ "type": 1 }is aPING: answer200with{ "type": 1 }. - Answer everything else within 3 seconds with an interaction response.
An endpoint in about twenty lines
js
import express from 'express'
import nacl from 'tweetnacl'
const PUBLIC_KEY = process.env.MOYMO_PUBLIC_KEY // from the Interactions tab
const app = express()
// The signature covers the raw bytes, so read them as they come.
app.post('/interactions', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.get('X-Signature-Ed25519')
const timestamp = req.get('X-Signature-Timestamp')
const verified = signature && timestamp && nacl.sign.detached.verify(
Buffer.from(timestamp + req.body.toString()),
Buffer.from(signature, 'hex'),
Buffer.from(PUBLIC_KEY, 'hex')
)
if (!verified) {
return res.status(401).send('invalid request signature')
}
const interaction = JSON.parse(req.body.toString())
if (interaction.type === 1) {
return res.json({ type: 1 })
}
if (interaction.type === 2 && interaction.data.name === 'hello') {
return res.json({ type: 4, data: { content: `Hello, ${interaction.member.user.display_name}!` } })
}
res.status(400).send('unknown interaction')
})
app.listen(3000)python
import os
from flask import Flask, request, jsonify
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
PUBLIC_KEY = VerifyKey(bytes.fromhex(os.environ["MOYMO_PUBLIC_KEY"]))
app = Flask(__name__)
@app.post("/interactions")
def interactions():
signature = request.headers.get("X-Signature-Ed25519", "")
timestamp = request.headers.get("X-Signature-Timestamp", "")
try:
PUBLIC_KEY.verify(timestamp.encode() + request.get_data(), bytes.fromhex(signature))
except (BadSignatureError, ValueError):
return "invalid request signature", 401
interaction = request.get_json()
if interaction["type"] == 1:
return jsonify(type=1)
return jsonify(type=4, data={"content": "Hello!"})Check every request
Moymo tests your endpoint with a request that is signed with a wrong key and expects 401. An endpoint that accepts it is refused. This is also the protection that matters: without verification anybody who learns your address can make your application do things.
Adding the endpoint
On the Interactions tab of your application in the Developer Portal, paste the address and choose Save and verify. Moymo sends two signed requests: a ping that must be answered with { "type": 1 }, and a ping signed with the wrong key that must be answered 401. If both are right, the address is kept. If not, the portal says what it got and keeps nothing.
The address has to be:
| https | Interactions carry people's words, so nothing is sent in the clear |
| Public | A name that points at a private network, a loopback address or an address Moymo itself uses is refused when it is called. Redirects are not followed |
| Quick | Answers within 3 seconds, and at most 64 KB |
Until the address is verified, nothing is sent to it, your commands do not appear in the slash menu, and members who press your buttons are told the application is not set up.
Every request has the user agent Moymo-Interactions/1.0.
The key
Each application has a key pair of its own. Make a new key if the public key was ever exposed: interactions pause until you have put the new key in your application and verified the address again.
Local development
The endpoint has to be reachable from the internet. A tunnel (such as a development tunnel from your host or cloudflared) gives your laptop a public https address for testing; set it as the endpoint while you work, then change it to your real address.
Handling interactions
Every interaction has the same envelope.
| Field | Type | Description |
|---|---|---|
id | UUID | The interaction's id |
application_id | UUID | Your application |
type | integer | The type |
token | string | Valid for 15 minutes: with it you follow up |
version | integer | 1 |
data | object | What was done: depends on the type |
server_id | UUID | Where it happened. Also sent as guild_id, so a bot written for Discord finds it |
channel_id | UUID | |
channel | object | { id, server_id, type, name } |
member | object | Who did it: { user, nick, roles, permissions }. permissions are theirs in the channel, as a decimal string |
app_permissions | string | What your bot may do in the channel, as a decimal string: check it before you try to do something it would refuse |
locale | string | The member's language. Moymo sends en for now |
message | object | For a component, the message it was on, as it was when pressed |
Command data
For type 2:
What Moymo sends your endpointHTTP
http
POST /interactions HTTP/1.1
Host: bot.example.com
Content-Type: application/json
User-Agent: Moymo-Interactions/1.0
X-Signature-Ed25519: 9f2c41e8…b07d
X-Signature-Timestamp: 1793729075
{
"id": "6a1e9d34-0b7c-4f52-8e13-9c4d2a7b5f06",
"application_id": "0f6c1a52-3b9e-4c1c-9d1b-6e7d2a8f4b10",
"type": 2,
"token": "moymo_it_Zk3pQ9…",
"version": 1,
"data": {
"id": "3b9d5e21-8c47-4a06-b1f2-6e0a7d4c9f38",
"name": "roll",
"type": 1,
"options": [{ "name": "sides", "type": 4, "value": 6 }],
"resolved": {}
},
"server_id": "5f1d2b8e-7a64-4d07-8d3a-0b9f6c4e2a11",
"guild_id": "5f1d2b8e-7a64-4d07-8d3a-0b9f6c4e2a11",
"channel_id": "2d7c4a90-1b3e-4f58-9a6c-8e0b5d3c7f14",
"channel": {
"id": "2d7c4a90-1b3e-4f58-9a6c-8e0b5d3c7f14",
"server_id": "5f1d2b8e-7a64-4d07-8d3a-0b9f6c4e2a11",
"type": "text",
"name": "general"
},
"member": {
"user": {
"id": "1f0c8d64-2a37-4f5b-9a0e-7c4d3b2e1a90",
"username": "ada",
"display_name": "Ada",
"bot": false
},
"roles": [],
"permissions": "3071"
},
"app_permissions": "3072",
"locale": "en"
}The body is sent as compact JSON; it is laid out here to be read. A nick is in member only when the member has one, and avatar in user only when they have a picture.
Options are in Discord's shape: { name, type, value }, with sub-commands and groups nesting their own options. Members, roles and channels arrive as ids in value, and resolved has what they are: resolved.users, resolved.roles, resolved.channels. For a command on a member data.target_id is the member and resolved.users has them; for a command on a message resolved.messages has the message.
Moymo has already checked every option against your command: the type, the bounds and lengths, the choices, and that a member, role or channel is in this server. An option your command does not declare never reaches you.
Component data
For type 3: data = { "custom_id": "…", "component_type": 2 } for a button, and for a menu also "values": [ … ] (the values of the options chosen, or the ids chosen in a user, role, mentionable or channel menu, with resolved as for commands).
Autocomplete data
For type 4: like a command, with focused: true on the option being typed (its value is what has been typed so far). Required options may be missing. Answer with up to 25 suggestions.
Modal data
For type 5: data = { "custom_id": "…", "components": [ … ] }, in the layout of the form you opened (see Using modals).
Seeing it work
- Set an endpoint and register a command (see Quickstart).
- In a server your bot is in, type
/in a text channel. Your command is listed under your application's name. - Run it. Your endpoint gets a request, answers, and the answer appears in the channel.
Where members can use this
Slash commands, buttons, menus and forms are in Moymo for Windows, in the Servers add-on 1.19.0 or newer. Members on an older version see your messages' text but not the buttons.