Rebuilding the admin interface

Long ago, adding items to Discord Dungeons was a big hassle, I didn't have the #!additem command and I had to add everything completely manually.
"This can't go on", I thought to myself, and as such, I built an admin interface for it.

Well, what's the issue? It works right?

Yeah, it works, that's the issue.

Originally, I meant for it to be an all-in-one deal for everyone, and you would've been able to manage the prefixes of your servers and your guilds and whatnot, but that never came since the code quickly grew out of hand as I had built it in Handlebars and kept everything in one file, seeing as all the backend logic was done with shudders PHP as it's what I knew at the time, and it was all I could use for the Discord OAuth2 needed to authenticate users to get the servers they were in and whatnot.

Since it's just been "working" and the code was pretty much unmanageable, I never built it for scalability, I built it to just work and as such, it never kept up with the updates of the bot itself, and doesn't support all items in the game.

Okay, so it's old and doesn't allow for updating?

Exactly, Jimmy.

Well, it does allow for updating, but it's such a pain that I'd rather not do it and instead I'd just like to rebuild it with something more modern and easy to use.

Enter React.js, a JavaScript library for building user interfaces.

Now, in a normal React application I'd be using all sorts of libraries alongside the base library such as React-Redux, but seeing as I'm not planning to make this a fully featured management application at the moment, I don't need to add Redux.

Sounds like you've got it all figured out, why are you writing this?

Not so fast, Jimmy.

There's a lot of challenges with this.

For one, I need a way to interface with the Discord OAuth2 in order to verify that the user accessing the application is actually allowed to do so. For another, I need a design for the whole thing, and neither of these is particularly easy to do from scratch.

Let's focus on the design for a bit.

For the design of the application, I didn't need it to be anything over-the-top designed, since it wasn't going to be used by a lot of people. As such, I opted to use Ant Design for the layout as it has React components that are ready to use and they look pretty good out of the box and can always be changed with some CSS.

And the functionality.

For the item adder, I didn't want to have everything in one file in the actual code.

To counteract this, I decided to do one component for each item type and as such keep the logic and data needed for each item type separate.

There's an issue though.

Regardless of how much you split your code when you're using Webpack as a bundler, it will all end up in a single massive file, which will take ages to load for the browser since it adds all your imported node modules, dependencies et cetera.

To get around this, you can use a feature of Webpack known as Code Splitting.

But - doing this in the regular way that Webpack wants you to will make your code harder to read and even messier than it already is, I mean, look at this!

// import async.js
import( './async.js' ).then( ( data ) => {
    console.log( data );
} );

Imagine having that all over your code when you want a new component.

Instead of doing all that and messing up my code and whatnot, I prefer using a small little plugin library called react-loadable which allows you to instead do this, and gives you the benefit of automatic code splitting:

import Loadable from 'react-loadable';
import Loading from './my-loading-component';

const LoadableComponent = Loadable({
  loader: () => import('./my-component'),
  loading: Loading,
});

export default class App extends React.Component {
  render() {
    return <LoadableComponent/>;
  }
}

With this, you can create a new loadable component from a function in your code and return that directly.

You said something about OAuth2?

Ah yes. Yes I did, Jimmy.

Since OAuth2 is a pain to implement by yourself, I decided to go search for one, all I could find was tutorial after tutorial and guide after guide using obscure libraries that would just cause massive overhead along with redux, neither of which are optimal since, well, I don't want a massive overhead and I don't have redux.

After googling around, I found the react-oauth-flow library, which basically is a drop-in solution for my problems.

Great, are you done?

I'm not Jimmy, didn't you take patience classes?

Originally, I was going to use react-router for this, after all, I had used it before in other projects.

Since I knew it was going to be used on the client-side, I decided to go with a HashRouter as they work without needing a server-side application alongside the client, so I got to work, added all the details to the Discord OAuth2 settings panel aaaand... nothing.

Well, there was something, an error saying "Invalid URL".

I changed the redirect URL aaaand... nothing, again.

After googling around, I figured I'd just have to go with some regex routing, which I quickly after realized wouldn't work.

Then I noticed in the URL of my browser, I noticed that the OAuth2 token had been added, and as such, I wrote a quick regex to extract it from the URL search query into my application; window.location.search.match(/code=(.*)/).

Next up, getting the user data from Discord.

To get the data from Discord, we need to get an authorization token to use, which we do with the token we got along with the client secret that our application has.

With the react-oauth-flow package I use, I can use it's built-in OauthReceiver component, and that's exactly what I'm going to do.

Once I got the authorization token in my applications state, I can use it with the fetch API built into most browsers to call the Discord API on the URL https://discordapp.com/api/users/@me with the header Authorization Bearer {token} to get the data of the logged in user.

Then I had to save it

Once I had the token, I didn't want to login through Discord every time I reloaded the page, so I needed a way to save the token somewhere I could access it later. For this, I decided to the use the localStorage API of browsers, which basically acts like a more permanent cookie.

Sounds cool, are you done?

Almost, there's still some things to do.

  • Better error handling - What happens if a fetch request fails?
  • Backend logic for adding the items to the game, the bot needs somewhere to fetch them from and implementing a database driver for this would need a backend anyways.
  • All the fields for the different item types, there's 17 of them and I've only got the fields for Dummy items in the application at the moment.
  • Cleaning the code. Creating something new, when doing it fast often leads to messy code no matter how hard you try so you just gotta bite the sour apple and refactor - that was the point of this whole thing, remember?

Yeah, but I meant with the blog post.

Not yet, Jimmy, we've got a backend to write!

I had a choice to make here.

Either I rewrite the current legacy system we have for items in order to not break the #!additem command, or I rewrite the #!additem command so I can just feed it raw JSON to add an item.

Now, if you've read my other blog posts and kept up with my rambling in the Official Discord, you should know that I'm going to choose to rewrite the entire thing.

Even more code?

Yep! More code!

Currently, the backend system takes the data it gets from a submitted HTML form and generates the data, generates a GUID and saves it to a file with the GUID as the name then returns that GUID to the client that posted the data.

Sounds fine to me.

To you, Jimmy, not to me.

This system doesn't allow for easy scalability since I need to edit the server code for each new item with each new field and every value it can have - not a good solution.

Instead, I wrote a quick Node.js script using express.js that takes some posted data and just saves it to a file and then I'll generate the JSON data needed from the client.

const express = require('express')
const fs = require("fs")
const app = express()

const uuid = () => {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
        const r = Math.random() * 16 | 0
        const v = c == 'x' ? r : (r & 0x3 | 0x8)
        return v.toString(16)
    })
}

app.use(express.json())

app.post('/', async (request, response) => {
    const fileName = uuid()
    fs.writeFile(`${uuid}.json`, request.body, err => {
        if(err){
            response.send(`ERROR: ${error}`)
        }else{
            response.send(fileName)
        }
    })
})

app.listen(3000)

Ugh, are you done now?

I am, Jimmy.

Now, normally I would give you a link to the finished thing, but this time I won't since it's neither complete yet, nor intended for use by people that aren't game admins or developers.

Regardless, it's a fun project to work on and will make my and the other developers lives much easier since we won't have to write all the data for the items manually anymore, and we can scale it to new items with ease.

Yes Jimmy, you can go play with Billy now.