A lightweight, modern Erlang library for building Discord bots
Minimal dependencies, no heavy frameworks. Just what you need to build Discord bots in Erlang.
Plug in your own module, function, or anonymous fun for command processing. Your way, your rules.
Full WebSocket support with automatic heartbeats, reconnection, and event handling.
Send and edit messages with simple function calls. No complex abstractions.
Handles connection drops gracefully with automatic reconnection logic.
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
- Go to Discord Developer Portal
- Create a New Application
- Go to "Bot" section and click "Add Bot"
- Enable MESSAGE CONTENT INTENT (required!)
- 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.
Starts the Discord bot client process.
%% 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}]).
Sends a message to a Discord channel.
ChannelId = <<"123456789012345678">>,
Content = <<"Hello, Discord! ð">>,
discord_bot_light_client:send_message(ChannelId, Content, Token).
Edits an existing message.
MessageId = <<"987654321098765432">>,
NewContent = <<"Updated message! âïļ">>,
discord_bot_light_client:edit_message(ChannelId, MessageId, NewContent, Token).
Your command handler must export:
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 |
â ïļ Important: You must enable these intents in the Discord Developer Portal:
Without these, your bot won't receive message events!
%%% 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}}.
%%% config/sys.config
[
{my_app, [
{discord_token, <<"YOUR_BOT_TOKEN_HERE">>},
{command_prefix, <<"!">>}
]}
].
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...">>.
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.
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.
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.
â The client automatically ignores messages from bots (including itself) to prevent infinite loops.
Check these common 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).
The client handles reconnection automatically. If you experience frequent disconnects:
Get started with Discord Bot Light today!
Need help or want to contribute?