Current section

Files

Jump to
lingo README.md
Raw

README.md

# Lingo

An Elixir library for interacting with the Discord API.

Currently covers:

- Slash commands, context menus, buttons, select menus, modals, and autocomplete
- Components V2 including sections, containers, media galleries, and more
- REST API with per-bucket rate limiting and automatic retries
- Gateway with sharding, caching, and reconnection: resumable sessions, zero-downtime resharding, shards (and cache reads) across nodes, ETF and zstd
- Interactions and webhook events over HTTP, with or without a gateway
- Command checks, cooldowns and an error callback
- OAuth2, and test helpers for bots
- Voice: play and receive audio, with Discord's end-to-end encryption (DAVE)
- Several bots in one VM, each with its own cache, rate limits and gateway
- Collectors for awaiting button clicks and reactions
- Permission, flag and role hierarchy helpers
- Embed, poll and component builders, CDN and Markdown helpers

No required dependencies: just OTP and Elixir 1.18+. Metrics go through
[`:telemetry`](https://hex.pm/packages/telemetry) when your app has it.

Check out the [docs](https://hexdocs.pm/lingo/) for the API reference.

## Install

```elixir
{:lingo, "~> 0.4.2"}
```

## Quick start

A bot is a module that declares its commands and implements the `Lingo.Bot`
callbacks it needs:

```elixir
defmodule MyBot do
  use Lingo.Bot

  command "ping", "Responds with pong"

  @impl true
  def handle_command("ping", ctx), do: reply!(ctx, "Pong!")

  @impl true
  def handle_event(:message_create, %{content: "hello"} = msg) do
    Lingo.Api.Message.create(msg.channel_id, "Hello back!")
  end
end
```

Add it to your supervision tree:

```elixir
children = [
  {MyBot,
   token: System.fetch_env!("BOT_TOKEN"),
   intents: [:guilds, :guild_messages, :message_content],
   register_commands: :global}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

`register_commands: :global` sends your commands to Discord once the bot is
ready. During development, `{:guild, guild_id}` updates instantly instead. You
can also call `Lingo.register_commands/1` yourself.

## Where things are

| Module | What it does |
|--------|--------------|
| `Lingo` | Starting bots, picking one, registering commands |
| `Lingo.Bot` | The behaviour your bot module implements |
| `Lingo.Context` | The interaction a callback handles: reading it and replying |
| `Lingo.Api.*` | The REST API, one module per resource (`Lingo.Api.Message`, `Lingo.Api.Guild`...) |
| `Lingo.Cache.*` | What the gateway and REST API have told the bot, one module per resource (`Lingo.Cache.Role.list/1`...); opt-in, configured in `Lingo.Cache` |
| `Lingo.Gateway` | Presence, shards and gateway requests |
| `Lingo.Voice` | Joining voice channels, playing and receiving audio |
| `Lingo.Component`, `Lingo.Embed`, `Lingo.Poll` | Building message content |
| `Lingo.Collector` | Awaiting clicks and reactions inline |
| `Lingo.Endpoint` | Interactions and webhook events over HTTP |
| `Lingo.OAuth2` | Authorizing users and calling the API as them |
| `Lingo.Testing` | Testing a bot without Discord |
| `Lingo.Telemetry` | Events for metrics |
| `Lingo.Helper.*`, `Lingo.Permissions`, `Lingo.Flags`, `Lingo.Format`, `Lingo.CDN` | Utilities |

## Commands with options

```elixir
command "greet", "Greet someone",
  options: [
    user(:target, "Who to greet", required: true),
    string(:message, "What to say")
  ]

@impl true
def handle_command("greet", ctx) do
  user = get_user(ctx, :target)
  reply!(ctx, "#{option(ctx, :message) || "Hey"} #{user.username}!")
end
```

Every callback is an ordinary function, so you match on whatever you like and
everything else is ignored.

## Components and collectors

```elixir
command "confirm", "Ask for confirmation"

@impl true
def handle_command("confirm", ctx) do
  reply!(ctx,
    content: "Are you sure?",
    components: [
      Lingo.Component.action_row([
        Lingo.Component.button(custom_id: "confirm:yes", label: "Yes", style: :success),
        Lingo.Component.button(custom_id: "confirm:no", label: "No", style: :danger)
      ])
    ]
  )
end

@impl true
def handle_component("confirm:" <> answer, ctx) do
  update!(ctx, content: "You said #{answer}.", components: [])
end
```

Or use collectors to await interactions inline:

```elixir
case Lingo.Collector.await_component(msg.id, timeout: 30_000) do
  {:ok, interaction} -> # handle the click
  :timeout -> # no response
end
```

## Events

Every event Discord sends reaches `handle_event/2`, named as Discord names it,
lowercased. Creates carry the resource, updates `%{old: old, new: new}`, and
everything else a struct of its own (`Lingo.Event` lists them all):

```elixir
@impl true
def handle_event(:guild_member_add, member) do
  Lingo.Api.Message.create(welcome_channel_id, "Welcome #{Lingo.Format.mention_user(member.user.id)}!")
end

def handle_event(:message_delete, %Lingo.Event.MessageDelete{channel_id: channel_id, old: old}) do
  if old, do: Lingo.Api.Message.create(channel_id, "Deleted: #{old.content}")
end

# old comes from the cache: nil unless messages are cached (see Lingo.Cache)
def handle_event(:message_update, %{old: %{content: before}, new: msg}) do
  IO.puts("#{msg.author.username} changed #{inspect(before)} to #{inspect(msg.content)}")
end
```

## Errors

API calls return `{:ok, result}`, `:ok` or `{:error, %Lingo.Error{}}`, and the
`!` helpers raise `Lingo.Error`:

```elixir
case Lingo.Api.Message.get(channel_id, message_id) do
  {:ok, message} -> message
  {:error, %Lingo.Error{status: 404}} -> nil
end
```

## Embeds

```elixir
Lingo.Embed.build(
  title: "Server Info",
  color: 0x5865F2,
  fields: [
    %{name: "Members", value: "1,234", inline: true},
    %{name: "Joined", value: Lingo.Format.timestamp(member.joined_at, :relative), inline: true}
  ]
)
```