Skip to content

Instantly share code, notes, and snippets.

@jb3
Last active February 3, 2023 21:31
Show Gist options
  • Save jb3/47c0496920dd61a50ed9c503d7d676e9 to your computer and use it in GitHub Desktop.
Save jb3/47c0496920dd61a50ed9c503d7d676e9 to your computer and use it in GitHub Desktop.
A simple implementation of Discord Slash Commands in Python with Microsoft Function Apps.

Dice rolling — Discord Interaction Edition

This is a very basic Discord Interaction designed to run on Azure Function Apps using the Python runtime.

Steps to get running

  1. Head to Discord and create a new application, take note of the Client ID, Client Secret and Interactions Public Key.
  2. Create a new Azure function app with the code in azure_function.py, make sure to add an app setting called INTERACTION_PUBLIC_KEY with the value taken from Discord. Make sure to add discord_interactions to your requirements.txt file, the function depends on it!
  3. Place the URL of your App Function HTTP trigger into the Discord Developer portal. If things are working it will allow you to save.
  4. Add the application to your server by visiting the OAuth2 URL generator in the Developer portal and creating a link with the application.commands scope.
  5. Run client_credentials.py and input your Client ID and Secret, take note of the returned access token in the access_token field.
  6. Run the register_command.py script with the token fetched through the client credentials script.
  7. Run the /dice command in your Discord server!

Going public!

At some point you'll want to have the commands appear by default in your users Discord servers without having to manually add them to the guild. To do this swap the URL in register_command.py for one that looks like the following:

url = f"https://discord.com/api/v8/applications/{APP_ID}/commands"

After you run the script once more the new command will begin to propagate across your applications Discord servers over a period of an hour.

Licensing

All code in this project is licensed under MIT. The full license can be found in the file named LICENSE.

import json
import os
import random
from typing import Any, Dict
import azure.functions as func
from discord_interactions import verify_key, InteractionResponseFlags, InteractionResponseType
def main(req: func.HttpRequest) -> func.HttpResponse:
# Discord security headers
signature = req.headers.get("X-Signature-Ed25519") or ''
timestamp = req.headers.get('X-Signature-Timestamp') or ''
# Interaction payload
data = req.get_json()
# Can we can verify the payload came from Discord?
if verify_key(req.get_body(), signature, timestamp, os.environ["INTERACTION_PUBLIC_KEY"]):
# Discord may ping our endpoint with a type 1 PING. We respond with a PONG.
if data["type"] == 1:
return func.HttpResponse(json.dumps({
"type": InteractionResponseType.PONG
}), status_code=200)
# Type 2 is a command execution.
if data["type"] == 2:
# Is the command our dice command?
if data["data"]["name"] == "dice":
# Return a JSON response from our do_dice_roll function.
return func.HttpResponse(json.dumps(do_dice_roll(data)))
# Anything else is probably a mistake, let's acknowledge and drop the message.
return func.HttpResponse(json.dumps({
"type": InteractionResponseType.ACKNOWLEDGE
}))
else:
# The payload did not come from Discord.
return func.HttpResponse(json.dumps({
"status": "invalid signature"
}), status_code=400)
def do_dice_roll(data: Dict[str, Any]) -> Dict[str, Any]:
# Convert the list of options into a dictionary
options = {item["name"]:item["value"] for item in data["data"]["options"]}
# Fetch user value or use one of our defaults.
maximum = options.get("maximum") or 6
rolls = options.get("rolls") or 1
# Maximum 20 rolls at once
if rolls > 20:
# Send an ephemeral (hidden) message with the error.
return {
"type": InteractionResponseType.CHANNEL_MESSAGE,
"data": {
"content": "You can have up to 20 rolls!",
"flags": InteractionResponseFlags.EPHEMERAL
}
}
# Maximum 50 as the largest dice faccec
if maximum > 50:
# Send an ephemeral message about this.
return {
"type": InteractionResponseType.CHANNEL_MESSAGE,
"data": {
"content": "You can only roll digits up to 50!",
"flags": InteractionResponseFlags.EPHEMERAL
}
}
fields = []
# For every dice roll, add a field to our embed.
for i in range(rolls):
# Pick a random value between our maximum.
val = random.randint(1, maximum)
fields.append({
"name": f"Roll #{i+1}",
"value": f"`{val}`",
"inline": True
})
# Create an embed to send back to Discord.
embeds = [
{
"title": "Dice rolls",
"color": 5814783,
"fields": fields
}
]
# Send back a message to Discord with the embeds.
return {
"type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
"data": {
"embeds": embeds
}
}
import requests
import base64
# Set some constants
API_ENDPOINT = 'https://discord.com/api/v8'
CLIENT_ID = input("OAuth2 Client ID: ")
CLIENT_SECRET = input("OAuth2 Client Secret: ")
def get_token():
# Construct the payload to send to Discord for the command update scope
data = {
'grant_type': 'client_credentials',
'scope': 'applications.commands.update'
}
# Headers as mandated by OAuth2 specification.
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
r = requests.post(
'%s/oauth2/token' % API_ENDPOINT,
data=data,
headers=headers,
# Authenticate with OAuth2 credentails.
auth=(CLIENT_ID, CLIENT_SECRET)
)
# Return the data from Discord.
return r.json()
# Print the response from Discord.
print(get_token())
MIT License
Copyright (c) 2017 Joe Banks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
import requests
# Grab some constants from our user.
APP_ID = input('OAuth2 Client ID: ')
GUILD_ID = input('Guild ID: ')
# Build the URL we are going to post our new commands to.
url = f"https://discord.com/api/v8/applications/{APP_ID}/guilds/{GUILD_ID}/commands"
# Command body
json = {
"name": "dice",
"description": "Roll a dice",
"options": [
{
"name": "Rolls",
"description": "Number of dice rolls to perform",
"required": False,
# Type 2 is an integer
"type": 4,
},
{
"name": "Maximum",
"description": "The maximum value of the dice rolls",
"required": False,
"type": 4
}
]
}
# Grab the auth for Discord
BEARER_TOKEN = input("OAuth2 Token (gain using client_credentials.py): ")
headers = {
"Authorization": f"Bearer {BEARER_TOKEN}"
}
# Send the request!
requests.post(url, headers=headers, json=json)
@AnkithAbhayan
Copy link

This is nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment