I released vaporware.

It wasn't on purpose.
Nov 21, 2022
7 min

Vaporware is software or hardware that has been advertised, but is not yet available to use. Obviously, releasing vaporware is not something to be proud of. Yet I managed to do so by accident. You can learn from my errors so as to not make the same mistake. So here’s how it happened and how I fixed it.

On March 1st, 2022, I set out to create Disgo, a Discord API Wrapper designed to be flexible, performant, secure, and thread-safe. Disgo aims to provide every feature in the Discord API along with optional rate limiting, structured logging, shard management, and caching… However, at that time, there was one issue: I had yet to commit a single line of code. Instead, I created a README outlining my vision for Disgo. What did I come up with?

The Design Documentation

Requests

In order to use most API Wrappers, you must learn three things.

  1. The Programming Language
  2. The API of the Consumer (Library)
  3. The API of the Producer (Discord)

This is because most programmers start creating libraries by using functions: Such that the function to send a message is ChannelMessage(…), while the function to respond to an interaction is InteractionRespond(…). Each function merits its own parameters, which requires you to read the documentation of the library to figure out how to do each task.

This is a huge waste of time and needlessly complex.

Disgo uses a single function to send HTTP requests (without type assertion or generics).

Send(*Client)

That’s it. Easy to remember and even easier to use. This function always maintains a single parameter: The client that the request is being sent by. However, you still need a way to specify what data is actually being sent. If you don’t use functions to do this, what’s the alternative? Structs.

// Create a Create Global Application Command request.
request := disgo.CreateGlobalApplicationCommand{
Name: "main",
Description: "A basic command.",
...
}

// Send the request.
command, err := request.Send(bot)

Each struct maintains all the data a person would need to send a valid request. That includes URL Query String Parameters, JSON fields, and files. Such that a developer would only have to understand two things to use Disgo.

  1. How to create a struct in Go.
  2. How to use the Discord API.

No more wasting time learning implementation details by the API consumer. How did I do it? Prior to the release of Disgo v0.10.0, I didn’t. That’s the issue: The entire library was simply a design that was being worked on. This led to a single incident much later on.

Events

Other Go libraries use type assertion or generics to translate JSON Event Data from Discord into an actual Go object that developers can reference during runtime. Disgo does not. It’s much faster as a result. Of course, this was only theoretically true prior to the release of Disgo v0.10.0. The implementation of this feature is discussed later in this article.

Disgo also maintains Automatic Intent Calculation; a feature no other library has.

Caching

The documentation behind the decision to use an optional cache was not released until later: #39. Regardless, that decision was made during the design of Disgo. As a summary, using a cache that is built-in to an HTTP library adds overhead, is complex at best, and incorrect at worst.

Updates

It’s been 6 years since the Discord API has been released, yet that same API continues to sustain numerous changes in a somewhat volatile manner. To be specific, Discord does NOT actually provide documentation for its API. Instead, a community is used to determine the current state of the API and document it. In addition, features are historically considered solely with the company in mind, which results in controversy when a feature has poor execution.

Discord has been prompted for a Machine Readable API numerous times to no avail. Such that changes are made to the Discord API frequently, but without an efficient strategy to apply those changes to API Consumers. For these reasons, Disgo needed to be able to maintain a high development velocity (during and after its initial creation).

Static Code Analysis is used to maintain formatted code that is free from known security issues. An extensive integration test that covers a majority of the Discord API was also planned. In addition, Go has a data race detector that allows us to detect and remove any potential data race or deadlock issues. Such that Disgo provides reliable data race protection without sacrificing the ability to run code concurrently.

Most important of all was the goal of achieving feature completion. A few libraries have done it, but none of them used Go.

Implementing The Design

The Go language uses JSON tags to unmarshal and marshal fields into its structs. However, this means that distinguishing between “empty” and “null” is important. The main Go API Wrapper at the time — DiscordGo — added these tags as it was developed, resulting in some features of the Discord API being unachievable without a fork. In order to solve this issue, we created a Go API Types Library for the Discord API called Dasgo. This library is built with a specification and is NOT implementation specific to Disgo; meaning that other Go API Wrappers can use it for their types.

When Dasgo is used by many people, the workload of the maintainers of Go Discord API Wrappers decreases drastically. This is because a new change to the Discord API’s data structures only merits a change to the underlying Go Types that Dasgo provides. Go itself provides hashed versioning which allows libraries to specify a specific version or branch to import. Such that one may maintain multiple versioned endpoints and every API version a Discord Bot may use.

Requests

The design documentation specifies how the Disgo API would work for HTTP Requests, but how did I actually implement it? As a reminder, I did not plan on using type assertion or generics: In Go, both have minor performance implications. As a result, I could not use structs as a parameter in the Send(*Client)function. Instead, receiver functions would be used.

func (r *CreateGlobalApplicationCommand) Send(bot *Client) (*ApplicationCommand, error) {
...
}

There is one issue, though. A receiver function such as this one must be created for every request. Yet the Discord API maintains 176 endpoints (not counting variations that make up a route). What were we going to do? Enter Copygen, a type-based code generator that I created to create type-to-type copy functions. With Copygen, I was able to use the types that we defined in Dasgo to generate a Send(*Client) function for every request.

The best part? Updates are as simple as running a single command.

While creating Disgo, I also discovered that the current rate limiting documentation was misleading. More on that in another article.

Events

The Discord Gateway is a TCP WebSocket Connection that sends packets of JSON or Binary data in a payload. This is straightforward to consume in JavaScript, since the language is untyped and interpreted. In contrast, Go is statically typed and compiled. As a result, other Go API Wrappers implement event handling by type asserting incoming payloads. The only way to avoid type assertion is by unmarshalling the payload directly into an object (struct). However, this isn’t a simple task since unmarshalling must be done concurrently (from a WebSocket Connection) as to not block the handling of other events.

Not to mention, that we needed to provide the developer a way to handle events.

In Disgo, Event Handlers are added or removed using the Handle and Remove function. Yet this is — again — a single function to represent hundreds of events. How did we do it without using type assertion? There was a compromise to be made since you cannot determine the event that is being handled without prior specification. To be specific, the Handle function could not accept a sole event as a parameter as this would require the use of reflection to determine which event object to unmarshal to.

// Add an event handler to the bot.
bot.Handle(disgo.FlagGatewayEventNameInteractionCreate, func(i *disgo.InteractionCreate) {
log.Printf("main called by %s", i.User.Username)
})

Instead, the user passes a string (specified by Dasgo) which tells us which event will be unmarshalled. The downside being that there can be user (developer) error involved if a mismatched string and event is passed to the function. As a result, developers are encouraged to handle the errors of these functions since an error — while unlikely — can occur. From this point, creating a goroutine (for the developer) to unmarshal the event payload, and a goroutine for each handler is straightforward.

Copygen was used to generate all necessary code for event handling.

Gateway Intents are annoying to deal with; especially in current API Wrappers. Every time you want to handle a new event, you must make sure it doesn’t require an intent. When it does, you must add that intent to your bot at whichever point in time that the API Wrapper specifies. That is a lot of work. Instead, Disgo — in pursuit of its no-reflection event handling — is able to calculate the intents the developer needs automatically. This allows the developer to focus more on their bot’s functionality and less on technical details.

The Release of Disgo

There were a few complications in finishing Disgo that led to it’s release in an unfinished state. To be specific, Disgo v0.10.0 supported the use of every request and event that the Discord API provided. However, features such as sharding, cache management, and the all-encompassing integration test were not implemented. In contrast, Disgo’s vision was committed to the main branch. Such that anyone could read it and unknowingly assume that its promises were already implemented. Not thinking about its interpretation, I made a v0.10.0 BETA release.

After all of this hard work, what did we get?

The Email

“Dear SwitchUpCB,

Your library disgo is a work of art… IF IT WOULD F*CKING WORK!!! Goddammit! I wasted at least three minutes importing this SHIT! Two more setting up the examples… Only to realize that this is not actually completed??? WHAT THE F*CK IS WRONG WITH YOU! Goddamn moron. Maybe don’t publish things before they are completed. huh? Huh?! So what now? What do I have to do to use this f*cking tool… What, are you gay? Y’know what. Lemme suck your d*ck. Lemme suck your d*ck man. Let bumbleboy7 suck your d*ck! LET BUMBLEBOY7 SUCK YOUR D*CK N****. LET. BUMBLEBOY7. SUCK. YOUR. D*CK!

Thanks,

bumbleboy7”

Yeah… Inspired by Filnobep… and totally fake.

Obviously, nothing bad actually happened. There was one user who ended up finding a major bug which stopped people from using Application Commands. I offered to compensate that user for their time, but they weren’t angry. In fact, there was already a disclaimer stating the limitations of v0.10.0. Perhaps, other libraries should create design documentation before getting caught up in Backwards Compatibility promises. The result would be a library as great as Disgo.

Use Disgo

Create a Discord Bot in Go: Disgo v0.10.1 was released on November 17, 2022.

Read More

link