Current section

Files

Jump to
discord_gleam src discord_gleam.gleam
Raw

src/discord_gleam.gleam

//// The primary module of discord_gleam. \
//// This module contains high-level functions to interact with the Discord API. \
//// But you can always implement stuff yourself using the low-level functions from the rest of the library. \
import booklet
import discord_gleam/discord/intents
import discord_gleam/discord/snowflake
import discord_gleam/event_handler
import discord_gleam/http/endpoints
import discord_gleam/internal/error
import discord_gleam/types/bot
import discord_gleam/types/channel
import discord_gleam/types/message
import discord_gleam/types/message_send_response
import discord_gleam/types/reply
import discord_gleam/types/slash_command
import discord_gleam/ws/commands/request_guild_members
import discord_gleam/ws/event_loop
import discord_gleam/ws/packets/interaction_create
import gleam/dict
import gleam/erlang/process
import gleam/list
import gleam/option
import gleam/otp/actor
/// Create a new bot instance.
///
/// Example:
/// ```gleam
/// import discord_gleam/discord/intents
///
/// fn main() {
/// let bot = discord_gleam.bot("TOKEN", "CLIENT_ID", intents.default()))
/// }
/// ```
pub fn bot(
token: String,
client_id: String,
intents: intents.Intents,
) -> bot.Bot {
bot.Bot(
token: token,
client_id: client_id,
intents: intents,
cache: bot.Cache(messages: booklet.new(dict.new())),
subject: process.new_subject(),
)
}
/// Instruction on how event loop actor should proceed after handling an event
///
/// - `Continue` - Continue processing with the updated state and optional
/// selector for custom user messages
/// - `Stop` - Stop the event loop
/// - `StopAbnormal` - Stop the event loop with an abnormal reason
pub opaque type Next(user_state, user_message) {
Continue(user_state, option.Option(process.Selector(user_message)))
Stop
StopAbnormal(reason: String)
}
/// Continue processing with the updated state. Use `with_selector` to add a
/// selector for custom user messages.
pub fn continue(state: user_state) -> Next(user_state, user_message) {
Continue(state, option.None)
}
/// Add a selector for custom user messages.
pub fn with_selector(
state: Next(user_state, user_message),
selector: process.Selector(user_message),
) -> Next(user_state, user_message) {
case state {
Continue(user_state, _) -> Continue(user_state, option.Some(selector))
_ -> state
}
}
/// Stop the event loop
pub fn stop() -> Next(user_state, user_message) {
Stop
}
/// Stop the event loop with an abnormal reason
pub fn stop_abnormal(reason: String) -> Next(user_state, user_message) {
StopAbnormal(reason)
}
/// The mode of the event handler
///
/// Simple mode is used for simple bots that don't need to handle custom user
/// state and messages. Can have multiple handlers.
///
/// Normal mode is used for bots that need to handle custom user state and
/// messages. Can have only one handler.
pub opaque type Mode(user_state, user_message) {
Simple(
bot: bot.Bot,
handlers: List(fn(bot.Bot, event_handler.Packet) -> Nil),
next: Next(user_state, user_message),
nil_state: user_state,
)
Normal(
bot: bot.Bot,
name: process.Name(user_message),
on_init: fn(process.Selector(user_message)) ->
#(user_state, process.Selector(user_message)),
handler: fn(bot.Bot, user_state, HandlerMessage(user_message)) ->
Next(user_state, user_message),
)
}
/// The message type for the event handler with custom user messages
pub type HandlerMessage(user_message) {
/// A discord packet
Packet(event_handler.Packet)
/// A custom user message
User(user_message)
}
/// Create a simple mode with multiple handlers
pub fn simple(
bot: bot.Bot,
handlers: List(fn(bot.Bot, event_handler.Packet) -> Nil),
) -> Mode(Nil, Nil) {
Simple(bot, handlers, Continue(Nil, option.None), Nil)
}
/// Create a normal mode with a single handler
///
/// `on_init` function is called once discord websocket connection is
/// initialized. It must return a tuple with initial state and selector for
/// custom messages. If there is no custom messages, user can pass the same
/// selector from the argument
pub fn new(
bot: bot.Bot,
on_init: fn(process.Selector(user_message)) ->
#(user_state, process.Selector(user_message)),
handler: fn(bot.Bot, user_state, HandlerMessage(user_message)) ->
Next(user_state, user_message),
) -> Mode(user_state, user_message) {
Normal(
bot,
process.new_name("normal_mode_user_message_subject"),
on_init,
handler,
)
}
/// Set process name for the event loop. Allows to use named subjects for custom
/// user messages in normal mode.
pub fn with_name(
mode: Mode(user_state, user_message),
name: process.Name(user_message),
) -> Mode(user_state, user_message) {
case mode {
Normal(..) -> Normal(..mode, name:)
Simple(..) -> mode
}
}
/// Start the event loop with a current mode.
///
/// Example:
/// ```gleam
/// import discord_gleam/discord/intents
/// import discord_gleam/event_handler
/// import gleam/erlang/process
///
/// fn main() {
/// let bot = discord_gleam.bot("TOKEN", "CLIENT_ID", intents.default())
///
/// let assert Ok(_) =
/// discord_gleam.simple(bot, [handler])
/// |> discord_gleam.start()
///
/// process.sleep_forever()
/// }
///
/// fn handler(bot: bot.Bot, packet: event_handler.Packet) {
/// case packet {
/// event_handler.ReadyPacket(ready) -> {
/// logging.log(logging.Info, "Logged in as " <> ready.d.user.username)
/// }
///
/// _ -> Nil
/// }
/// }
/// ```
///
pub fn start(
mode: Mode(user_state, user_message),
) -> Result(
actor.Started(process.Subject(event_loop.EventLoopMessage)),
actor.StartError,
) {
let state = booklet.new(dict.new())
event_loop.start_event_loop(
to_internal_mode(mode),
"gateway.discord.gg",
False,
"",
state,
)
}
fn to_internal_mode(
mode: Mode(user_state, user_message),
) -> event_handler.Mode(user_state, user_message) {
case mode {
Simple(bot, handlers, next, nil_state) ->
event_handler.Simple(bot, handlers, to_internal_next(next), nil_state)
Normal(bot, name, on_init, handler) -> {
let handler = fn(bot, user_state, msg) {
handler(bot, user_state, internal_to_handler_message(msg))
|> to_internal_next()
}
event_handler.Normal(bot, name, on_init, handler)
}
}
}
fn internal_to_handler_message(
msg: event_handler.HandlerMessage(user_message),
) -> HandlerMessage(user_message) {
case msg {
event_handler.DiscordPacket(packet) -> Packet(packet)
event_handler.User(msg) -> User(msg)
}
}
fn to_internal_next(
next: Next(user_state, user_message),
) -> event_handler.Next(user_state, user_message) {
case next {
Continue(user_state, opt) -> event_handler.Continue(user_state, opt)
Stop -> event_handler.Stop
StopAbnormal(reason) -> event_handler.StopAbnormal(reason)
}
}
/// Send a message to a channel.
///
/// Example:
/// ```gleam
/// import discord_gleam
///
/// fn main() {
/// ...
///
/// let msg = discord_gleam.send_message(
/// bot,
/// "CHANNEL_ID",
/// "Hello world!",
/// [] // embeds
/// )
/// }
pub fn send_message(
bot: bot.Bot,
channel_id: String,
message: String,
embeds: List(message.Embed),
) -> Result(message_send_response.MessageSendResponse, error.DiscordError) {
let msg = message.Message(content: message, embeds: embeds)
endpoints.send_message(bot.token, channel_id, msg)
}
/// Create a DM channel with a user. \
/// Returns a channel object, or a DiscordError if it fails.
pub fn create_dm_channel(
bot: bot.Bot,
user_id: String,
) -> Result(channel.Channel, error.DiscordError) {
endpoints.create_dm_channel(bot.token, user_id)
}
/// Send a direct message to a user. \
/// Same use as `send_message`, but use user_id instead of channel_id. \
/// `discord_gleam.send_direct_message(bot, "USER_ID", "Hello world!", [])`
///
/// Note: This will return a DiscordError if the DM channel cant be made
pub fn send_direct_message(
bot: bot.Bot,
user_id: String,
message: String,
embeds: List(message.Embed),
) -> Result(Nil, error.DiscordError) {
let msg = message.Message(content: message, embeds: embeds)
endpoints.send_direct_message(bot.token, user_id, msg)
}
/// Reply to a message in a channel.
///
/// Example:
///
/// ```gleam
/// import discord_gleam
///
/// fn main() {
/// ...
///
/// discord_gleam.reply(bot, "CHANNEL_ID", "MESSAGE_ID", "Hello world!", [])
/// }
/// ```
pub fn reply(
bot: bot.Bot,
channel_id: String,
message_id: String,
message: String,
embeds: List(message.Embed),
) -> Result(Nil, error.DiscordError) {
let msg =
reply.Reply(content: message, message_id: message_id, embeds: embeds)
endpoints.reply(bot.token, channel_id, msg)
}
/// Kicks an member from an server. \
/// The reason will be what is shown in the audit log.
///
/// Example:
///
/// ```gleam
/// import discord_gleam
///
/// fn main() {
/// ...
///
/// discord_gleam.kick_member(bot, "GUILD_ID", "USER_ID", "REASON")
/// }
///
/// For an full example, see the `examples/kick.gleam` file.
pub fn kick_member(
bot: bot.Bot,
guild_id: String,
user_id: String,
reason: String,
) -> Result(Nil, error.DiscordError) {
endpoints.kick_member(bot.token, guild_id, user_id, reason)
}
pub fn ban_member(
bot: bot.Bot,
guild_id: String,
user_id: String,
reason: String,
) -> Result(Nil, error.DiscordError) {
endpoints.ban_member(bot.token, guild_id, user_id, reason)
}
/// Deletes an message from a channel. \
/// The reason will be what is shown in the audit log.
///
/// Example:
/// ```gleam
/// import discord_gleam
///
/// fn main() {
/// ...
///
/// discord_gleam.delete_message(
/// bot,
/// "CHANNEL_ID",
/// "MESSAGE_ID",
/// "REASON",
/// )
/// }
///
/// For an full example, see the `examples/delete_message.gleam` file.
pub fn delete_message(
bot: bot.Bot,
channel_id: String,
message_id: String,
reason: String,
) -> Result(Nil, error.DiscordError) {
endpoints.delete_message(bot.token, channel_id, message_id, reason)
}
/// Edits an existing message in a channel. \
/// The message must have been sent by the bot itself.
pub fn edit_message(
bot: bot.Bot,
channel_id: String,
message_id: String,
content: String,
embeds: List(message.Embed),
) -> Result(Nil, error.DiscordError) {
let msg = message.Message(content: content, embeds: embeds)
endpoints.edit_message(bot.token, channel_id, message_id, msg)
}
/// Wipes all the global slash commands for the bot. \
/// Restarting your client might be required to see the changes. \
pub fn wipe_global_commands(bot: bot.Bot) -> Result(Nil, error.DiscordError) {
endpoints.wipe_global_commands(bot.token, bot.client_id)
}
/// Wipes all the guild slash commands for the bot. \
/// Restarting your client might be required to see the changes. \
pub fn wipe_guild_commands(
bot: bot.Bot,
guild_id: String,
) -> Result(Nil, error.DiscordError) {
endpoints.wipe_guild_commands(bot.token, bot.client_id, guild_id)
}
/// Registers a global slash command. \
/// Restarting your client might be required to see the changes. \
pub fn register_global_commands(
bot: bot.Bot,
commands: List(slash_command.SlashCommand),
) -> Result(Nil, #(slash_command.SlashCommand, error.DiscordError)) {
list.try_each(commands, fn(command) {
case endpoints.register_global_command(bot.token, bot.client_id, command) {
Ok(_) -> Ok(Nil)
Error(err) -> Error(#(command, err))
}
})
}
/// Registers a guild-specific slash command. \
/// Restarting your client might be required to see the changes. \
pub fn register_guild_commands(
bot: bot.Bot,
guild_id: String,
commands: List(slash_command.SlashCommand),
) -> Result(Nil, #(slash_command.SlashCommand, error.DiscordError)) {
list.try_each(commands, fn(command) {
case
endpoints.register_guild_command(
bot.token,
bot.client_id,
guild_id,
command,
)
{
Ok(_) -> Ok(Nil)
Error(err) -> Error(#(command, err))
}
})
}
/// Make a basic text reply to an interaction.
pub fn interaction_reply_message(
interaction: interaction_create.InteractionCreatePacket,
message: String,
ephemeral: Bool,
) -> Result(Nil, error.DiscordError) {
endpoints.interaction_send_text(interaction, message, ephemeral)
}
/// Used to request all members of a guild. The server will send
/// GUILD_MEMBERS_CHUNK events in response with up to 1000 members per chunk
/// until all members that match the request have been sent.
///
/// Nonce can only be up to 32 bytes. If you send an invalid nonce it will be
/// ignored and the reply member_chunk(s) will not have a nonce set.
pub fn request_guild_members(
bot: bot.Bot,
guild_id guild_id: snowflake.Snowflake,
option option: request_guild_members.RequestGuildMembersOption,
presences presences: option.Option(Bool),
nonce nonce: option.Option(String),
) -> Nil {
request_guild_members.request_guild_members(
bot,
guild_id,
option,
presences,
nonce,
)
}