💻 Dev Challenge: Discord Bots
Dev Challenge #3 - Come along to see how Challenge 3 could be solved. We've built a Discord Bot!
Hi, it's Developer Week at Cloudflare and it's been a great week so far. I can't believe we're only up to day four.
I'm Gretchen and I'm here with Luke and we're going to talk through the Developer Challenge that was around getting a Discord bot up and running for our Discord server.
If you've got any questions you want us to answer as we go through, send them to Cloudflare TV.
So there's an email address, it is livestudio at Cloudflare .tv.
Sorry, I have to keep reading that to make sure I send you to the right place.
So those come straight through to us and then we can answer them as we go.
So don't save them for the end because it might not make sense, it might not connect to what we're doing.
So it's livestudio at Cloudflare.tv.
Now the Discord bot challenge asked you to work through a tutorial on CodeStacker, but then we asked you to take it a bit further.
So most of our other challenges let you follow along and kind of, they were a bit more gentle.
This was the first one where we took the training worlds off a bit and said, here's some docs, go be free, let us know where you got to.
And it was amazing to see how people took that.
They went, I don't know, I was blown away to be honest. We got some great results and people went and used KB storage perfectly as it was meant to be used.
But Luke is here to show us one of the ways he went through it and I'd like to put in here that there are so many ways to do these challenges.
What we're doing here is just, I guess, what we came up with or what worked for us at that time on that day.
So don't take it as a yours wasn't right. This is just one vague selection.
I always kind of think coding is a bit like cooking. You see the recipe and it's a bit of a guideline and then off you go.
So take this in that vein. And also I'm not great at cooking.
So there's that. But Luke, did you watch the whole tutorial we gave people before we started or did you just dive straight in and start writing code?
Tell me about your process. So I guess I'm kind of a make it up as you go kind of cook.
So I did watch, I started watching the tutorial. But once I got the sense that it was going to be more of a step-by-step, here's how you do it.
I said thank you for the start and I dove right in myself.
So I will say I did watch the rest of it after I started diving in just to make sure I wasn't completely off track.
It was always nice to touch base. But my take on the challenge and what we'll be walking through today together is more of the bare bones.
What happens underneath the, what was going on with the tutorial so you could see all the nuts and bolts and how everything worked together.
That's a great point because the tutorial had a really solid package in it, right?
That did a lot of the work for us. And to be fair, that was part of why it was the tutorial because it made life a bit easier.
It was a bit like a packet cake mix if you like. We just had to do the eggs and the stirring.
But you had a look at it, what it would be like without that as well.
Right. So I will call that out as we go along. But definitely a shout out to Greg Brimble for putting together that package.
But what we will go through is exactly what that package is doing for you.
So if you want to get your hands dirty, go hang out in the weeds with me like this is for you.
But at any point in time, what we go through together now can just be replaced by that package.
It is a really solid package.
And we talked to Greg earlier this morning on our Discord channel and he mentioned that there was a couple of things he's updating with it because when it was originally written, I think the Discord slash commands were still in beta.
So there's a couple of things that are being changed on that end as well.
But anyway, should we have a look at your code and see what's happening? Sure.
So my very first step, one, of course, was to create a package.
So I just have a video for us here where I went through just to get that out of the way.
Right. So I go ahead and call it dev week challenge server.
So that's created. And once that is done, that takes us to our Discord developers panel.
And I got to all of these steps just by following the Discord docs, of course.
Right. So this is the actual application itself where we see an application ID, a public key, and some OAuth information.
We'll get back to that in a second. Actually, I think we should get back into that right now.
So these become our environment secrets, our environment variables, or our secret bindings.
These, just like any other Wrangler application, you attach those as bindings as part of your config and it will be available to your worker.
So I'm using TypeScript here. So I'm just declaring these right off the bat saying, I have client IDs, I have a client secret, and a public key.
Those are going to be my binding names. Similarly, we will have a KB namespace.
So we pull that in. And that KB namespace will be called animals throughout the example.
And now we have to get the actual code. What is a Discord app?
How are we going to be dealing with this? So we already talked about creating the app.
There is an authorization and an attachment step, lopping those in for the same step.
And then we're actually going to be receiving commands. So obviously, the important stuff or the stuff we care about is receiving commands, because that's what we're going to be reacting to.
But we have to get there first. So what I'm hearing is that was just like three steps for you, right?
And in terms of a big overview, you were like, I'm going to create the app through the Discord interface, then I'm going to get the authorization stuff set up.
And part of that was using Wrangler to put your secrets in.
Yeah, so I connected to Discord right off the bat.
I created that app just so I had these identifiers and passwords reserved to me ahead of time.
Some clients, not Discord, but other services do take a little bit of time to set that stuff up.
So for me, I just did it right off the bat, just so I already had those values ready to go.
I like it. That's a good way of doing it.
So then your step three would be to get the information and the interaction.
Right, to actually use the thing, right? Sounds so simple when you break it down to three steps.
Well, it does take a little bit of heavy lifting to get there.
So our step two now, right, the authorize and connect phase.
So Discord itself doesn't care about this. All it cares about is that it knows where it's going to be sending data, and it knows that it's allowed to.
So that's why authorization part is important, and that's why our endpoint has to actually receive code.
But for now, we're going to worry about the attachment and the authorization.
So because it doesn't care, it being Discord, we get to call these whatever we want.
So we have this get, you know, the root address, and that's going to be our authorized step.
Dive into that in a second. And then we're going to separate out a setup phase.
So, you know, step 2A, authorize. Authorization is as simple as just clicking a button, right?
So this is Discord's.
This is a redirect to Discord's authorized service. And like every other, not many people like or care to know about OAuth, but OAuth requires that you actually send scopes and that the user click a button to say, yes, I actually accept these and I'm allowing this to happen.
So that's what's happening here. There's an OAuth 2 authorized endpoint.
We're requesting the application command scope, which we can see here.
I forgot to call this out, but Discord, the entire time I was following along the Discord documentation for slash commands.
And. Good documentation, isn't it? Oh, yeah.
A bit of a read. It wasn't just for fun. It was useful. I found them easy to follow and thorough on the pieces I needed them to be thorough with.
Exactly. Like, I've never made a Discord bot before, but, you know, I just went down this page and by the end of it, we have the working bots.
We'll get there. But so here's the authorizing your application thing.
So this is where they call out right off the bat that applications.commands scope is necessary.
So we pass that in here along with our client ID and just send that off.
I think I have a video for this right now. Sounds simple when you know how.
Actually, I'm going to come back to that later.
So step 2B, if we go back to here, 2B is our actual setup phase.
So setup is how we're telling Discord what this app is supposed to do.
We are defining the application. So if we look this part up, this is our first integration with KV.
So there's a bit going on here. But again, in an OAuth 2 application, it's all about access control and authorization control.
So even though we authorize the application to connect to the Discord server, we still have to verify and authenticate every request on behalf of the application.
There's a lot more to it than that, but we'll just go with that little truncated definition.
So as part of, again, this is all following the documentation page, but anytime we are sending or changing the definition of the application itself, we have to be re -verified.
So step one of this definition phase is we have to get a new token.
So the grant type, in the weeds, it's just an OAuth 2 thing.
Our scope, again, we're asking to update a command. And then we're passing in our client ID that we grabbed before.
That came from this guy here, our client ID from our Discord page, as well as our- The interface is pretty straightforward, right, to get stuff from.
It's not hidden away. Oh, yeah. No, they make it very clear.
And on the worker side, it's also very clear to define these places once and grab them when you need them.
And then move on. Yeah. So we have our client ID, our client secret that goes in, and Discord does its thing.
It sends us back an access token.
So with that access token, again, we are acting on behalf of our command and saying, please take these new commands.
This is an overwrite.
This is just sending up a command definition saying, take this token that we grabbed earlier and create this command definition.
This is where KV comes in the very first time.
What is BLEP? BLEP was the command that the Discord docs were referring to in the slash command tutorial.
So it's not my name. I don't even know what it means.
But effectively, everything in here, aside from the KV stuff, is directly from the Discord docs.
So anyone who goes through it, should look very familiar.
Got you. So it's not like your kitten that you've got or a dog's name or anything.
It's from the Discord docs. That makes sense. Do you know what I didn't point out earlier is that the actual project was so you could use a slash command to get an image rendered.
So you could go through and choose an animal, a big animal or a little animal.
Hence, the description is send a random photo of an animal.
I should have added that earlier so we knew what it was we were building towards.
Right. So I did see that in the video. The video also was going through the Discord docs and I think they did change the BLEP command, but it was definitely animal pictures and animal baby pictures as well.
So again, TypeScript stuff here, but our KV record, we called it the animals binding before.
But the KV store is staring animal objects that have an image and a source, both of which are strings.
So when we ask for our command, we're asking for this animal choices key, retrieving it as JSON, and then using all of those values to create this name value array of objects.
And so choices, all this stuff gets sent directly to Discord and Discord needs this choices array of objects with both the name and a value where name is the visually displayed value and value is the value that we're going to be caring about on the way back in.
So if I hop over to my KV, I will prove that this is all here, right?
So what we were requesting animal choices and animal choices right now is cat, chicken, chimpanzee, dog, fox, hedgehog, and puffin.
And you may be wondering where all this data came from. And this is just a load function that I threw in here.
We'll call it a step 1.5, right? So in terms of the three steps to the entire project.
So step 1.5, I just hard coded a bunch of this data.
And then on my routing side, I added my own get slash load. Sorry, I have a typo here.
Get load, which called a load function, which we pull in directly from commands.
That's great, because I was going to ask how you got the data into KV, but there you are.
Yeah, just for this, I mean, obviously, you would have some sort of like actual API that would be controlling KV and managing this data.
But for me, I wanted my Discord app to have a very limited set of options and control what those keys looked like.
So as it goes through, it's creating this animal choices array, as well as actually creating those animal key objects resulting in animal colon colon chicken, we have an image here.
And then somewhere down here, we have a source as well.
We'll see those again later. But a great little hack when you're just trying to get something up and running and see what it looks like.
Yeah, perfect for testing.
And you can always remove code in between deploys. And just so you're not constantly reloading and rewriting the data or don't want your users to do that for you.
So great way to go. So back to a video. So we, at this point, sorry to make you dizzy.
At this point, we so this no longer exists, right? We've removed that we created our authorized step and we created our setup step.
So at this point, we'll build and deploy the Wrangler app. These are our manual administration endpoints.
And so we actually have to hook up the hook up the server.
So my we missed it very quickly. But my please play. So this is my workers .dev domain, I just go to the root domain, and it's going to redirect me to a discord page.
So this is where I am choosing which server I am actually attaching the application to select the application, I click authorize.
That's it, you know, the scopes have gone through discord side says it's authorized.
Thank you very much like you are done here.
So the second phase is we have to write. Yeah, it's super easy. It was just a redirect.
The second phase is we have to call setup manually. So if we call setup.
And that's in the docs too, right?
And it just says go slash setup, and we're going to say okay.
Basically, so these were our two, these are our two routes that we decided to call them, you know, the homepage and the setup.
But like, as far as the discord docs are concerned, we just had to send discord this information saying like, these are my command definitions.
And this is how we get you to be authorized.
So that actually brings up a great point. Because now that we've called both of those, we can delete it, we can completely remove it from our application.
And I show that here in between deploys, where I remove the setup from the top of the page, highlight the two routes, delete it and save.
So at this point, we never we never have to call it again, unless we are changing our command definition.
So if we want to add more animals, or add more slash commands, we have to like read the setup command and then call it ourselves.
Important thing to call out here with anytime we call the set slash setup, we have to wait an hour, not because of us, but because of discord.
So why is that? So does discord just need enough time to check what we've done?
What is what's going on there? Yeah. So if we go back to here, the if we go back to commands.
So all of our application, I'll hide this, all of our endpoints, or our endpoint in this example, at least, I believe in the video, too, they are accessing the applications, the global application definition.
So because our application can be installed by multiple, multiple, multiple servers, discord needs time to actually propagate these changes so that any users of our of our discord app can't have time to update.
So the the way around that and I and the documentation calls us out is some they basically recommend setting up a separate server as a guild server, which I've never done.
But the guild servers can update instantly because it's in terms of endpoints.
This is something like guilds slash guild ID, right, which means that at this point, there is only a single destination destination that is receiving that command definition.
So it doesn't need time to propagate globally. So it's just a guilds like one particular server, and you just just doing that one.
So it's quicker.
That's a very Yeah, something around that space. So that time wait is just what it is at the moment.
It's because we're working on a global scale. You know, I'm sure a discord expert can yell at us and tell us we're missing something.
But yeah, to my understanding that that's the difference. And it's a good excuse to get up and make a cup of tea.
So at this point, we've done step one, step two, we've created applications, it's authorized, we can start receiving stuff.
So, you know, our main, our main function here is post slash interaction. I have one more video for us.
interaction. That was very quick. But the I'll show you here.
I'm sorry, I'm making me dizzy. So inside the discord applications, there's an interactions endpoint URL.
So we know we are discord calls out, they will only send HTTP HTTP post methods.
And they're asking us where, where do we send this? So because I call this slash interaction, as does the video, I, this is where it's going to go.
So any command that comes through will be sent to that destination, and we have to handle it.
So let's look at what that actually entails. interaction. As per OAuth interactions, there's always some sort of signature and timestamp value.
Discord has a great little write up for us here, security and authorization, an entire function, and I pretty much just copy pasted this, right?
So like, we have to get a x signature, ed 255, whatever header, as well as the timestamp header.
And then they recommend this package that I've pulled in as well, and pass those values to the package to determine whether or not we are verified.
So same thing here, get the two header values, get the raw text, again, as per the documentation, and then determine whether or not those values line up.
If they are, it's a verified request as a valid request that discord itself actually sent us and it hasn't been tampered with with some third party.
So if it's not verified, we just kick it out, say 401, you know, invalid, we don't want this.
If it is valid, we are free to continue.
So we, they always send the data as JSON. So we call rec.json to get the data itself.
And I'm going to slide this over one more time. Because it said pings required in your notes, right?
It is. So they call that out right off the top.
So receiving an interaction, you, your endpoint must be prepared to act a ping message.
So act in their terms, I think it's here is, you know, if type is one, then you have to reply type is one, and they call that act.
Right? So right off the top, we parse it and say if type is one, we reply with type is one, right?
So that in discord terms, I, they just send off these ping requests to make sure that the endpoint is still alive.
I think if the endpoint goes down, they won't like allow the command to be run, or they'll just fail instantly.
But basically, it's a health check.
Now, everything else is up to us, right? We're free to do whatever we want.
And the documentation, as well as the video as one function.
But if we look at what discord actually sends us, it's something like this, right?
Where they'll send you a type, they'll send you data containing options, saying what, like, arguments for pass the command, as well as the command name itself.
And in this time, they moved away from blep for this example, probably because they couldn't come up with valid options for blep.
I don't know. But, you know, you also have access to member data, right, which we're not using here.
But you can easily, you know, react to the command name coming in, and switch, switch the handler based off of that name.
We didn't do that here, because we had just the one. So we have a blep handler that we that receives the options itself.
So the blep handler converts an array of choices into an object just for easier sake of use on my part.
And then we construct a key name based off of those values that come in. So, you know, an animal, and this lines up directly with the command definition itself.
So back in our command, we said we have an animal option, as well as an only small option.
So I do love the spelling of only small. It made me laugh the first time, and it continues to make me laugh.
Very Internet spelling. So, you know, when our values come in, we have an animal option and an only small option.
So that's where those two come from.
And then I have this two key name helper, which I always love to do, because it guarantees that my keys are constructed the same way.
So the animal comes in the first time, and then that's right, as the first parameter, and then the small Boolean comes in too.
So if it's a, you know, a chicken, we're going to have animal colon colon chicken.
And then if small was true, we append colon colon small to it.
And you may remember, that's exactly what we did when we loaded our data.
So, you know, these guys here too, they all use the two key name value. So then moving on, we now have our key inside of a handler.
We then request for the second time, we finally are touching our KB namespace.
So inside our KB namespace, we are asking to get that key and parse it as JSON.
It should always match.
So this not value check should never actually happen, because again, we control our own data set.
We control the Discord options, and we control basically everything.
So at this point, we're just returning a response for Discord. So they care about a 200 mostly, but you can also customize what gets returned.
So there's a whole bunch of options there.
I have a link here for anyone who's interested, but you could return anything you want.
It's nuts. That's terrifying. Yeah. There are some limitations, but it's very flexible.
We've had a quick question come through the email.
So the email, if you've got any questions, is livestudio at Cloudflare.tv. And I think I can talk to this one easily myself too.
It was, is the project code going to be available for reference?
So it's a definite yes. We have a Discord community server, sorry, a worker's community Discord server.
And we'll be posting all the links in there.
So actually, if you just Google Cloudflare workers Discord, you'll find a link to join the server and everything will be posted in there.
So this was challenge number three, or new user challenge number three.
So in that channel, we'll post a link to the repo so you can get the source code and tinker around with that yourself.
Right. Yeah. So yeah, I mean, the TypeScript stuff, definitely you need to play with it to see like where it helps.
But so quick rewind, we fetched our value out of KB, made sure it actually existed.
And now we're sending something back down to Discord.
So we're sending an image in bed.
We created some alt text description and then the provider and the image itself.
So just to prove that it actually exists, I do have a video of it. And we'll do a couple live to as per your choices, Gretchen, but we called slash blep, we have a list of who my keystrokes are covering.
But, you know, we chose a baby chimpanzee, we're going to do another one, blep, we're doing a chimpanzee again.
And this time, we didn't say small true.
So we got an adult chimpanzee. Baby ones are always so much cuter in all of these.
We can do a couple more too. So this is all live now.
So let's go with a puff. I want to see a puffin. Okay. Call this slash blep, puffin, baby or adult size?
Baby, only small. I have to do tab again, only small true.
That's a baby. I've never seen one before. And I'll, I always love adult. But actually, so only small is optional, but I added in anyway, so I don't know how we get from that, from this to that, but we do.
So if you were going to do this, and not follow the tutorial and not just have to do, well, you didn't have to do baby animals and adult animals.
It just was what the tutorial said. If you had free reign in the universe, what would you do with your slash command?
So I, I really wanted to do a Pokemon thing.
And I actually saw on the discord today that someone did.
But you can have sub slash commands, right? So you can have slash Pokemon, and then the first argument be like a sub command.
So slash Pokemon catch slash Pokemon list slash Pokemon, like, what do I own?
Right? And then a series of other arguments.
So I really, I guess a really challenge or a call to action for anyone else is to build a sub command Pokemon command that uses KV.
So you can have a math dot random chance of catching a named Pokemon, you know, so slash Pokemon catch Pikachu, and then on the server side, it's a 5050 or an 8020 chance of catching it.
And then your user discord username is used to say which Pokemon do you own at any point in time.
So I think we know what you're up to this weekend, Luke.
Probably, yes. Hey, we really appreciate you watching. We'll share the GitHub repo on the discord channel.
So if you haven't joined that yet, once again, just Google Cloudflare Workers discord community, and you'll find the link, it should be easy enough to do.
We're back tomorrow to work through the next challenge.
So we really hope to see you there. And then we're taking a break over the weekend before we get back into some of the solutions for the bigger challenges next week.
Once again, thanks for spending your time with us. Take care. Bye.