Discord have changed the way bots work quite a few times. Recently, though, they built a system that lets you create and register “slash commands” — commands that you can type into the Discord chat and which do things, like /hello
— and which are powered by “webhooks”. That is: when someone uses your command, it sends an HTTP request to a URL of your choice, and your URL then responds, and that process powers what your users see in Discord. Importantly, this means that operating a Discord bot does not require a long-running server process. You don’t need to host it somewhere where you worry about the bot process crashing, how you’re going to recover from that, all that stuff. No daemon required. In fact, you can make a complete Discord bot in one single PHP file. You don’t even need any PHP libraries. One file, which you upload to your completely-standard shared hosting webspace, the same way you might upload any other simple PHP thing. Here’s some notes on how I did that.
The Discord documentation is pretty annoying and difficult to follow; all the stuff you need is in there, somewhere, but it’s often hard to find where, and there’s very little that explains why a thing is the way it is. It’s tough to grasp the “Zen” of how Discord wants you to work with their stuff. But in essence, you’ll need to create a Discord app: follow their instructions to do that. Then, we’ll write our small PHP file, and upload it; finally, fill in the URL of that PHP file as the “interactive endpoint URL” in your newly-created app’s general information page in the Discord developer admin. You can then add the bot to a server by visiting the URL from the “URL generator” for your app in the Discord dev admin.
The PHP file will get sent blocks of JSON, which describe what a user is doing — a command they’ve typed, parameters to that command, or whatever — and respond with something which is shown to the user — the text of a message which is your bot’s reply, or a command to alter the text of a previous message, or add a clickable button to it, and the like. I won’t go into detail about all the things you can do here (if that would be interesting, let me know and maybe I’ll write a followup or two), but the basic structure of your bot needs to be that it authenticates the incoming request from Discord, it interprets that request, and it responds to that request.
Authentication first. When you create your app, you get a client_public_key
value, a big long string of hex digits that will look like c78c32c3c7871369fa67
or whatever. Your PHP file needs to know this value somehow. (How you do that is up to you; think of this like a MySQL username and password, and handle this the same way you do those.) Then, every request that comes in will have two important HTTP headers: X-Signature-ED25519
and X-Signature-Timestamp
. You use a combination of these (which provide a signature for the incoming request) and your public key to check whether the request is legitimate. There are PHP libraries to do this, but fortunately we don’t need them; PHP has the relevant signature verification stuff built in, these days. So, to read the content of the incoming post and verify the signature on it:
/* read the incoming request data */$postData = file_get_contents('php://input');/* get the signature and timestamp header values */$signature = isset($_SERVER['HTTP_X_SIGNATURE_ED25519']) ? $_SERVER['HTTP_X_SIGNATURE_ED25519'] : "";$timestamp = isset($_SERVER['HTTP_X_SIGNATURE_TIMESTAMP']) ? $_SERVER['HTTP_X_SIGNATURE_TIMESTAMP'] : "";/* check the signature */$sigok = sodium_crypto_sign_verify_detached( hex2bin($signature), $timestamp . $postData, hex2bin($client_public_key));/* If signature is not OK, reject the request */if (!$sigok) { http_response_code(401); die();}
We need to correctly reject invalidly signed requests, because Discord will check that we do — they will occasionally send test requests with bad signatures to confirm that you’re doing the check. (They do this when you first add the URL to the Discord admin screens; if it won’t let you save the settings, then it’s because Discord thinks your URL returned the wrong thing. This is annoying, because you have no idea why Discord didn’t like it; best bet is to add lots of error_log()
logging of inputs and outputs to your PHP file and inspect the results carefully.)
Next, interpret the incoming request and do things with it. The only thing we have to respond to here is a ping
message; Discord will send them as part of their irregular testing, and expects to get back a correctly-formatted pong
message.
$data = json_decode($postData);if ($data->type == 1) { // this is a ping message echo json_encode(array('type' => 1)); // response: pong die();}
The magic numbers there (1 for a ping
, 1 for a pong
) are both defined in the Discord docs (incoming values being the “Interaction Type” field and outgoing values the “Interaction Callback Type”.)
After that, the world’s approximately your oyster. You check the incoming type
field for the type of incoming thing this is — a slash command, a button click in a message, whatever — and respond appropriately. This is all stuff for future posts if there’s interest, but the docs (in particular the “receiving and responding and “message components” sections) have all the detail. For your bot to provide a slash command, you have to register it first, which is a faff; I wrote a little Python script to do that. You only have to do it once. The script looks approximately like this; you’ll need your APP_ID and your BOT_TOKEN from the Discord dashboard.
importrequests,jsonMY_COMMAND={"name":'doit',"description":'Do the thing',"type":1}discord_endpoint=_f"https://discord.com/api/v10/applications/{APP_ID}/commands"requests.request("PUT",discord_endpoint,json=[MY_COMMAND],headers={"Authorization":f"Bot {BOT_TOKEN}","User-Agent":'mybotname (myboturl, 1.0.0)',})
Once you’ve done that, you can use /doit
in a channel with your bot in, and your PHP bot URL will receive the incoming request for you to process.