Risk-Free trial

Open Source Discord Interactions SDK for Elixir: Build Bots with Outbound Webhooks

A digital graphic with a wavy blue abstract background featuring the Elixir logo (a shaded purple droplet) on the left and the Discord logo (a white game controller face) on the right, separated by a white plus sign. Below the logos, bold white text reads 'DISCORD INTERACTIONS IN ELIXIR'.

Discord is a communication platform designed for creating communities, allowing users to chat via text, voice, or video. It’s especially popular among gamers and online creators but has expanded into education, business, and general social use.

Here at profiq, we are also using Discord with some of our clients. Due to the fact that Discord is free and it provides a lot of features similar to Slack or Microsoft Teams, it’s an ideal tool in a Startup environment where every buck counts. With modern projects, the chat platform becomes the heartbeat of the company, sometimes even more important than email itself. That’s why it’s crucial to be able to extend these chat platforms with bots, applications and other custom integrations.

How to extend Discord?

The Discord ecosystem supports multiple ways to extend its functionality and to integrate your product into the app.

  • Inbound webhooks are an easy way to send Discord messages automatically by sending HTTP requests to a Discord provided URL. This is a great way to implement Discord notifications in response to events happening in your app but as a one-way channel it does not support any form of interactivity.
  • WebSocket client is the most flexible way to make a Discord bot. The client can listen to incoming Discord messages or other events and reply to them, allowing full two-way communication. However, it requires the client to maintain the WebSocket connection the whole time.
  • Outbound webhooks are the newest way to write a Discord app. They are the middle ground between regular inbound webhooks and a full WebSocket client. Upon triggering an “interaction” (user executing a command or interacting with the components in an already-sent response to a command), Discord sends a HTTP request to the application-provided URL. It does not support the full range of functionality as the WebSocket client but still is a very attractive solution for existing apps that want to add integration with the platform.

What did we do?

When looking at Elixir libraries for Discord, we noticed that all of them target the WebSocket client. Therefore, we decided to make a library for the new outbound interaction webhooks.

Why Elixir? Because we have experience with the language since 2016, have used it on projects for customers like Bill.com and PDQ, and because developers love it! In fact, parts of Discord’s backend are also written in Elixir.

Introduction to the SDK

The SDK package can be added to your project by adding the following line to the deps section of mix.exs:

def deps do
  [
    {:discord_interactions, "~> 0.1.0"}
  ]
end

Now we can start writing our own Discord command handler. Let’s start with a simple example, a /hello command which merely greets the user who sent it. Create a new module in your application and add the following:

defmodule YourApp.Discord do
  use DiscordInteractions

  interactions do
    application_command "hello" do
      description("A friendly greeting command")
      handler(&hello/1)
    end

  end
end

The above is our command definition. Now we need to implement the command handler itself:

def hello(itx) do
  # Access user information from the interaction
  user = get_in(itx, ["member", "user", "username"])

  response = InteractionResponse.channel_message_with_source()
             |> InteractionResponse.content("Hello, " <> user)

  {:ok, response}
end

Now that we defined our command and implemented it, we can use the SDK to do the rest. There are two main parts to our SDK, command registration and interaction handling itself. Let’s register our commands with Discord on application startup by adding a task to the supervisor tree:

def start(_type, _args) do
  children = [
    # Other children...
    YourAppWeb.Endpoint,
    {DiscordInteractions.CommandRegistration, YourApp.Discord} # added this line
  ]

  opts = [strategy: :one_for_one, name: YourApp.Supervisor]
  Supervisor.start_link(children, opts)
end

We now need to add an endpoint which will handle the incoming interactions. Add the following line to your application’s Phoenix router:

forward "/discord", DiscordInteractions.Plug, YourApp.Discord

The plug also requires access to both the parsed request JSON and the raw body. The Phoenix application template helpfully already configures a Plug.Parsers plug for JSON, we just need to add our body reader implementation to cache the raw request body. Update the plug definition in the endpoint module as follows:

plug Plug.Parsers,
  parsers: [:urlencoded, :multipart, :json],
  pass: ["*/*"],
  json_decoder: Phoenix.json_library(),
  body_reader: {DiscordInteractions.CacheBodyReader, :read_body, []} # added this line

The only thing left is configuring the Discord secrets and variables. There are three values that you need to retrieve from the Discord developer portal: the public key, application ID and bot token.

Screenshot of the Discord Developer Portal displaying the "General Information" section for an application named "Elixir test." The left sidebar highlights the "General Information" tab under the "Settings" category. In the main content area, details such as the app icon, name, description, and tags are shown. A red box emphasizes the "Application ID" and "Public Key" fields, each with a "Copy" button beside them. Screenshot of the Discord Developer Portal showing the "Bot" section for an application named "Elixir test." The left sidebar highlights the "Bot" tab under "Settings." A red box highlights the "Token" field area with a note stating that tokens can only be viewed once and a "Reset Token" button beneath it.

Let’s configure the application to get these values from environment variables by adding the following to the runtime config:

config :discord_interactions,
  public_key: System.get_env("DISCORD_PUBLIC_KEY"),
  bot_token: System.get_env("DISCORD_BOT_TOKEN"),
  application_id: System.get_env("DISCORD_APPLICATION_ID")

This finishes the setup in our application. Let’s deploy it and add it to our Discord server! (You might need to restart your client in order for our new command to appear in the command auto-complete suggestions.)

Screenshot of a Discord chat interface showing the slash command input. The user has typed /hello, and the suggestion box displays a matching command titled /hello with the description "Greets you with a mention." The command is associated with the bot named "Elixir test."

After executing the command, you should see something like this in the chat window:

The image shows a Discord message where a user named "User" interacted with a bot by typing the command "hello." The bot, labeled "Elixir test," responds with a message saying, "Hello, @User!" The interface includes the "APP" tag, indicating it's a message from a bot on a server.

Conclusion

One of the things we learned during this project is the great flexibility and versatility that Discord offers to bot authors. Creating a Discord application can be as simple as exposing a simple REST endpoint, and when combined with services like Cloud Run, your changes can go out in literal seconds.

It is also impressive how the Elixir ecosystem offloads work from library developers by making it so simple to publish packages to Hex.pm, which aside from hosting your packages also automatically generates documentation from your source code.

You can find the full library with example code on our GitHub, be sure to also check HexDocs for full API documentation. The library is of course completely open source under MIT license, so feel free to contribute or open new feature requests.

Leave a Reply

Related articles

Tags