Discord Bot Light

A lightweight, modern Erlang library for building Discord bots

âœĻ Simple & Lightweight 🚀 Production Ready ðŸ“Ķ v1.0.0 ⚡ Gateway API

Why Discord Bot Light?

ðŸŠķ

Lightweight

Minimal dependencies, no heavy frameworks. Just what you need to build Discord bots in Erlang.

🔧

Flexible Handlers

Plug in your own module, function, or anonymous fun for command processing. Your way, your rules.

🌐

Modern Gateway

Full WebSocket support with automatic heartbeats, reconnection, and event handling.

ðŸ“Ą

Simple API

Send and edit messages with simple function calls. No complex abstractions.

â™ŧïļ

Auto-Reconnect

Handles connection drops gracefully with automatic reconnection logic.

Quick Start

1. Installation

Add discord_bot_light to your rebar.config:

%% rebar.config
{deps, [
    {discord_bot_light, "1.0.0"}
]}.

%% Also required:
%% - jsone (JSON)

            

2. Create Your Bot Token

📝 Discord Developer Portal

  1. Go to Discord Developer Portal
  2. Create a New Application
  3. Go to "Bot" section and click "Add Bot"
  4. Enable MESSAGE CONTENT INTENT (required!)
  5. Copy your bot token

3. Create a Command Handler

%%% my_bot_commands.erl
-module(my_bot_commands).
-export([handle_message/4]).

handle_message(<<"!level">>, ChannelId, Author, Token) ->
    UserId = maps:get(<<"id">>, Author),
    Level = calculate_user_level(UserId),
    Username = maps:get(<<"username">>, Author),
    Response = <<Username/binary, " is level ", 
                (integer_to_binary(Level))/binary, "! 🎖ïļ">>,
    discord_bot_light_client:send_message(ChannelId, Response, Token);

handle_message(Content, _ChannelId, Author, _Token) ->
    %% Track all messages
    ets:update_counter(bot_state, message_count, 1),
    
    %% Update user stats
    UserId = maps:get(<<"id">>, Author),
    case ets:lookup(bot_state, {user, UserId}) of
        [] -> ets:insert(bot_state, {{user, UserId}, 1});
        [{_, Count}] -> ets:insert(bot_state, {{user, UserId}, Count + 1})
    end,
    ok.

%% Helper functions
get_top_users() ->
    AllUsers = ets:match(bot_state, {{user, '$1'}, '$2'}),
    Sorted = lists:sort(fun([_, C1], [_, C2]) -> C1 > C2 end, AllUsers),
    lists:sublist(Sorted, 10).

format_leaderboard(Users) ->
    Lines = [<<"🏆 Top Users:\n">> | 
             [format_user_line(N, UserId, Count) || 
              {N, [UserId, Count]} <- lists:zip(lists:seq(1, length(Users)), Users)]],
    iolist_to_binary(Lines).

format_user_line(Rank, UserId, Count) ->
    RankBin = integer_to_binary(Rank),
    CountBin = integer_to_binary(Count),
    <<RankBin/binary, ". User ", UserId/binary, ": ", CountBin/binary, " msgs\n">>.

calculate_user_level(UserId) ->
    case ets:lookup(bot_state, {user, UserId}) of
        [] -> 1;
        [{_, Count}] -> trunc(math:sqrt(Count / 10)) + 1
    end.

API Reference

start_link(Token, Options) -> {ok, Pid} | {error, Reason}

Starts the Discord bot client process.

Token :: binary()
Your Discord bot token
Options :: proplist()
[{command_handler, Module | {Module, Function} | Fun}]
%% Example with module
discord_bot_light_client:start_link(Token, [{command_handler, my_bot}]).

%% Example with MFA
discord_bot_light_client:start_link(Token, [{command_handler, {my_bot, handle}}]).

%% Example with anonymous function
Handler = fun(Content, ChannelId, Author, Token) ->
    io:format("Received: ~p~n", [Content])
end,
discord_bot_light_client:start_link(Token, [{command_handler, Handler}]).
send_message(ChannelId, Content, Token) -> ok | {error, Reason}

Sends a message to a Discord channel.

ChannelId :: binary()
The Discord channel ID
Content :: binary()
The message text (max 2000 characters)
Token :: binary()
Your bot token
ChannelId = <<"123456789012345678">>,
Content = <<"Hello, Discord! 👋">>,
discord_bot_light_client:send_message(ChannelId, Content, Token).
edit_message(ChannelId, MessageId, NewContent, Token) -> ok | {error, Reason}

Edits an existing message.

ChannelId :: binary()
The Discord channel ID
MessageId :: binary()
The message ID to edit
NewContent :: binary()
The new message text
Token :: binary()
Your bot token
MessageId = <<"987654321098765432">>,
NewContent = <<"Updated message! ✏ïļ">>,
discord_bot_light_client:edit_message(ChannelId, MessageId, NewContent, Token).

Command Handler Interface

Your command handler must export:

handle_message(Content, ChannelId, Author, Token) -> ok | {error, Reason}
Content :: binary()
The message text from the user
ChannelId :: binary()
The channel where the message was sent
Author :: map()
Information about the message author
Keys: id, username, discriminator, bot
Token :: binary()
Your bot token (for sending responses)

Author Map Structure

Key Type Description
id binary() User's Discord ID
username binary() User's display name
discriminator binary() User's discriminator (e.g., "0001")
bot boolean() Whether the user is a bot
avatar binary() | null User's avatar hash

Configuration Guide

Required Intents

⚠ïļ Important: You must enable these intents in the Discord Developer Portal:

  • MESSAGE CONTENT - Required to read message content
  • GUILD MESSAGES - Required to receive server messages

Without these, your bot won't receive message events!

Integration with OTP Applications

%%% my_app_sup.erl - Supervisor example
-module(my_app_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).

start_link() ->
    supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
    Token = application:get_env(my_app, discord_token, <<>>),
    Handler = my_bot_commands,
    
    ChildSpecs = [
        #{
            id => discord_bot,
            start => {discord_bot_light_client, start_link, 
                     [Token, [{command_handler, Handler}]]},
            restart => permanent,
            shutdown => 5000,
            type => worker
        }
    ],
    
    {ok, {{one_for_one, 5, 10}, ChildSpecs}}.

Environment Configuration

%%% config/sys.config
[
    {my_app, [
        {discord_token, <<"YOUR_BOT_TOKEN_HERE">>},
        {command_prefix, <<"!">>}
    ]}
].

Best Practices

1. Don't Hardcode Tokens

Always load tokens from environment variables or config files:

%% Good ✅
Token = os:getenv("DISCORD_BOT_TOKEN"),

%% Good ✅
Token = application:get_env(my_app, discord_token),

%% Bad ❌
Token = <<"MTIzNDU2Nzg5MDEyMzQ1Njc4.ABCDEF.xyz...">>.

2. Rate Limiting

Discord has rate limits. Implement backoff for sending many messages:

send_with_backoff(ChannelId, Content, Token) ->
    case discord_bot_light_client:send_message(ChannelId, Content, Token) of
        ok -> ok;
        {error, rate_limited} ->
            timer:sleep(1000),
            send_with_backoff(ChannelId, Content, Token);
        Error -> Error
    end.

3. Command Pattern Matching

Use binary pattern matching for efficient command parsing:

%% Efficient ✅
handle_message(<<"!help">>, ChannelId, _, Token) -> ...;
handle_message(<<"!kick ", UserId/binary>>, ChannelId, _, Token) -> ...;

%% Less efficient ❌
handle_message(Content, ChannelId, _, Token) ->
    case binary:split(Content, <<" ">>) of
        [<<"!help">>] -> ...;
        [<<"!kick">>, UserId] -> ...
    end.

4. Error Handling

handle_message(Content, ChannelId, Author, Token) ->
    try
        process_command(Content, ChannelId, Author, Token)
    catch
        error:Reason ->
            io:format("Error processing command: ~p~n", [Reason]),
            discord_bot_light_client:send_message(
                ChannelId,
                <<"❌ An error occurred!">>,
                Token
            )
    end.

5. Ignore Bot Messages

✅ The client automatically ignores messages from bots (including itself) to prevent infinite loops.

Troubleshooting

Bot Not Responding

Check these common issues:

  1. ✅ MESSAGE CONTENT intent enabled in Developer Portal
  2. ✅ Bot is in the server (invited with correct scopes)
  3. ✅ Bot has permission to read/send messages in the channel
  4. ✅ Token is correct and not expired
  5. ✅ Your command handler is properly registered

Connection Issues

%% Check if process is running
whereis(discord_bot_light_client).

%% Check process info
erlang:process_info(Pid).

%% Enable debug logging (if implemented)
application:set_env(discord_bot_light, log_level, debug).

WebSocket Disconnects

The client handles reconnection automatically. If you experience frequent disconnects:

Ready to Build Your Bot?

Get started with Discord Bot Light today!

Community & Support

Need help or want to contribute?

handle_message(<<"!hello">>, ChannelId, Author, Token) -> Username = maps:get(<<"username">>, Author, <<"Unknown">>), Response = <<"Hello, ", Username/binary, "!">>, discord_bot_light_client:send_message(ChannelId, Response, Token); handle_message(<<"!ping">>, ChannelId, _Author, Token) -> discord_bot_light_client:send_message(ChannelId, <<"Pong!">>, Token); handle_message(_, _, _, _) -> ok.

4. Start Your Bot

%% In your application or shell
Token = <<"YOUR_DISCORD_BOT_TOKEN">>,
Handler = my_bot_commands,
{ok, Pid} = discord_bot_light_client:start_link(Token, [{command_handler, Handler}]).

🎉 That's it! Your bot is now running and will respond to !hello and !ping commands.

Bot Examples

Moderation Bot

Basic moderation commands with permission checks:

%%% moderation_bot.erl
-module(moderation_bot).
-export([handle_message/4]).

handle_message(<<"!kick ", Rest/binary>>, ChannelId, Author, Token) ->
    case is_moderator(Author) of
        true ->
            UserId = extract_user_id(Rest),
            kick_user(ChannelId, UserId, Token);
        false ->
            discord_bot_light_client:send_message(
                ChannelId, 
                <<"❌ You don't have permission!">>, 
                Token
            )
    end;

handle_message(<<"!ban ", Rest/binary>>, ChannelId, Author, Token) ->
    case is_admin(Author) of
        true ->
            UserId = extract_user_id(Rest),
            ban_user(ChannelId, UserId, Token);
        false ->
            discord_bot_light_client:send_message(
                ChannelId, 
                <<"❌ Admin only!">>, 
                Token
            )
    end;

handle_message(<<"!warn ", Rest/binary>>, ChannelId, _Author, Token) ->
    [UserId, Reason] = binary:split(Rest, <<" ">>),
    Message = <<"⚠ïļ Warning: ", Reason/binary>>,
    discord_bot_light_client:send_message(ChannelId, Message, Token);

handle_message(_, _, _, _) ->
    ok.

%% Helper functions
is_moderator(Author) ->
    %% Check roles from Author map
    Roles = maps:get(<<"roles">>, Author, []),
    lists:member(<<"moderator">>, Roles).

is_admin(Author) ->
    Roles = maps:get(<<"roles">>, Author, []),
    lists:member(<<"admin">>, Roles).

Fun Commands Bot

Entertainment commands for your server:

%%% fun_bot.erl
-module(fun_bot).
-export([handle_message/4]).

handle_message(<<"!8ball ", Question/binary>>, ChannelId, _Author, Token) ->
    Answers = [
        <<"Yes, definitely!">>,
        <<"Maybe...">>,
        <<"Don't count on it.">>,
        <<"Ask again later.">>
    ],
    Answer = lists:nth(rand:uniform(length(Answers)), Answers),
    Response = <<"ðŸŽą ", Answer/binary>>,
    discord_bot_light_client:send_message(ChannelId, Response, Token);

handle_message(<<"!roll">>, ChannelId, Author, Token) ->
    Roll = integer_to_binary(rand:uniform(100)),
    Username = maps:get(<<"username">>, Author),
    Response = <<Username/binary, " rolled: ðŸŽē ", Roll/binary>>,
    discord_bot_light_client:send_message(ChannelId, Response, Token);

handle_message(<<"!coinflip">>, ChannelId, _Author, Token) ->
    Result = case rand:uniform(2) of
        1 -> <<"Heads! 🊙">>;
        2 -> <<"Tails! 🊙">>
    end,
    discord_bot_light_client:send_message(ChannelId, Result, Token);

handle_message(<<"!joke">>, ChannelId, _Author, Token) ->
    Jokes = [
        <<"Why don't programmers like nature? It has too many bugs! 🐛">>,
        <<"A SQL query walks into a bar, walks up to two tables and asks... 'Can I join you?' ðŸŧ">>,
        <<"There are only 10 types of people: those who understand binary and those who don't. 😄">>
    ],
    Joke = lists:nth(rand:uniform(length(Jokes)), Jokes),
    discord_bot_light_client:send_message(ChannelId, Joke, Token);

handle_message(_, _, _, _) ->
    ok.

Utility Bot

Useful server utilities and information:

%%% utility_bot.erl
-module(utility_bot).
-export([handle_message/4]).

handle_message(<<"!serverinfo">>, ChannelId, _Author, Token) ->
    Info = <<"📊 Server Information\n"
           "Members: 1,234\n"
           "Created: 2020-01-01\n"
           "Region: US-West">>,
    discord_bot_light_client:send_message(ChannelId, Info, Token);

handle_message(<<"!userinfo">>, ChannelId, Author, Token) ->
    Username = maps:get(<<"username">>, Author),
    UserId = maps:get(<<"id">>, Author),
    Info = <<"ðŸ‘Ī User: ", Username/binary, "\n"
           "ID: ", UserId/binary>>,
    discord_bot_light_client:send_message(ChannelId, Info, Token);

handle_message(<<"!poll ", Question/binary>>, ChannelId, _Author, Token) ->
    Message = <<"📊 Poll: ", Question/binary, "\n\nReact with 👍 or 👎">>,
    discord_bot_light_client:send_message(ChannelId, Message, Token);

handle_message(<<"!remind ", Rest/binary>>, ChannelId, Author, Token) ->
    Username = maps:get(<<"username">>, Author),
    Response = <<"⏰ I'll remind ", Username/binary, ": ", Rest/binary>>,
    discord_bot_light_client:send_message(ChannelId, Response, Token),
    %% Set up timer for reminder
    spawn(fun() -> 
        timer:sleep(60000), %% 1 minute
        Reminder = <<"🔔 Reminder: ", Rest/binary>>,
        discord_bot_light_client:send_message(ChannelId, Reminder, Token)
    end);

handle_message(_, _, _, _) ->
    ok.

Advanced: Stateful Bot with ETS

Bot with persistent state using ETS tables:

%%% advanced_bot.erl
-module(advanced_bot).
-export([start/1, handle_message/4]).

start(Token) ->
    %% Create ETS table for state
    ets:new(bot_state, [named_table, public, set]),
    ets:insert(bot_state, {message_count, 0}),
    
    Handler = ?MODULE,
    discord_bot_light_client:start_link(Token, [{command_handler, Handler}]).

handle_message(<<"!stats">>, ChannelId, _Author, Token) ->
    [{_, Count}] = ets:lookup(bot_state, message_count),
    Response = <<"📈 Messages processed: ", 
                (integer_to_binary(Count))/binary>>,
    discord_bot_light_client:send_message(ChannelId, Response, Token);

handle_message(<<"!leaderboard">>, ChannelId, _Author, Token) ->
    %% Get top users from ETS
    Users = get_top_users(),
    Board = format_leaderboard(Users),
    discord_bot_light_client:send_message(ChannelId, Board, Token);