//// 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/bot import discord_gleam/discord/snowflake.{type Snowflake} import discord_gleam/event_handler import discord_gleam/http/applications import discord_gleam/http/channels import discord_gleam/http/guilds import discord_gleam/http/users import discord_gleam/internal/error 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/commands/update_presence import discord_gleam/ws/event_loop import discord_gleam/ws/gateway_state import gleam/erlang/process import gleam/list import gleam/option.{type Option, None, Some} import gleam/otp/actor /// 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(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, 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, 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, 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 discord_gleam/bot /// import gleam/erlang/process /// /// fn main() { /// let bot = bot.new("TOKEN", "CLIENT_ID") /// /// 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(gateway_state.new()) event_loop.start_event_loop( mode: to_internal_mode(mode), host: "gateway.discord.gg", reconnect: False, session_id: "", state_ets: 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: Snowflake(snowflake.Channel), message: message.Message, ) -> Result(message_send_response.MessageSendResponse, error.DiscordError) { channels.send_message(bot.token, channel_id, message) } /// 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: Snowflake(snowflake.User), ) -> Result(channel.Channel, error.DiscordError) { users.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: Snowflake(snowflake.User), message: message.Message, ) -> Result(message_send_response.MessageSendResponse, error.DiscordError) { users.send_direct_message(bot.token, user_id, message) } /// 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: Snowflake(snowflake.Channel), message_id: Snowflake(snowflake.Message), message: message.Message, ) -> Result(message_send_response.MessageSendResponse, error.DiscordError) { let msg = reply.Reply(message_id: message_id, reply: message) channels.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: Snowflake(snowflake.Guild), user_id: Snowflake(snowflake.User), reason: String, ) -> Result(Nil, error.DiscordError) { guilds.kick_member(bot.token, guild_id, user_id, reason) } pub fn ban_member( bot: bot.Bot, guild_id: Snowflake(snowflake.Guild), user_id: Snowflake(snowflake.User), reason: String, ) -> Result(Nil, error.DiscordError) { guilds.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: Snowflake(snowflake.Channel), message_id: Snowflake(snowflake.Message), reason: String, ) -> Result(Nil, error.DiscordError) { channels.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: Snowflake(snowflake.Channel), message_id: Snowflake(snowflake.Message), message: message.Message, ) -> Result(Nil, error.DiscordError) { channels.edit_message(bot.token, channel_id, message_id, message) } /// 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) { applications.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: Snowflake(snowflake.Guild), ) -> Result(Nil, error.DiscordError) { applications.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 applications.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: Snowflake(snowflake.Guild), commands: List(slash_command.SlashCommand), ) -> Result(Nil, #(slash_command.SlashCommand, error.DiscordError)) { list.try_each(commands, fn(command) { case applications.register_guild_command( bot.token, bot.client_id, guild_id, command, ) { Ok(_) -> Ok(Nil) Error(err) -> Error(#(command, err)) } }) } /// 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.Bot, guild_id guild_id: snowflake.Snowflake(snowflake.Guild), option option: request_guild_members.RequestGuildMembersOption, presences presences: Option(Bool), nonce nonce: Option(String), ) -> Nil { request_guild_members.request_guild_members( bot, guild_id, option, presences, nonce, ) } /// Used to update the bot's activity/presence /// Use with the bot passed from a handler, the normal bot object will not have the necessary websocket data to send the packet pub fn update_presence( bot: bot.Bot, presence: update_presence.Presence, ) -> Nil { update_presence.update_presence(bot, presence) }