# Lamma for developers > Build apps, bots and webhooks for Lamma servers. Lamma lets you build **apps**: bots that join servers, answer slash commands and react to what happens there. It also has **webhooks**, secret links that post into a channel without signing in. Both use a small, versioned API that is close to Discord's on purpose, so most bot code feels familiar. > [!NOTE] > Apps are in beta. During the beta, building apps is open to developers we invite. To ask for access, email [lamma.help@gmail.com](mailto:lamma.help@gmail.com). ## What you can build - **Bots** that read and send messages, add reactions and keep track of a server's channels, roles and members. See [Apps and installs](https://lammapp.sa/developers/apps). - **Slash commands** that people pick by typing `/` in the message box. See [Slash commands](https://lammapp.sa/developers/commands). - **Mod-log and welcome bots** that hear about joins, leaves, bans and audit log entries. See [Gateway events](https://lammapp.sa/developers/events). - **Webhooks** that post updates from your own service into a channel. See [Webhooks](https://lammapp.sa/developers/webhooks). - **Link cards** that design how your website's links look when someone shares them. See [Link cards](https://lammapp.sa/developers/link-cards). ## How it fits together | Piece | What it does | Address | | --- | --- | --- | | REST API | Read servers, channels and messages; send, edit and react | `https://lammapp.sa/v1` | | Gateway | A WebSocket that pushes events to your bot as they happen | `wss://lammapp.sa/v1/gateway` | | Webhooks | Post into one channel with a secret URL | `https://lammapp.sa/api/webhooks/{id}/{secret}` | | SDKs | [lamma.js](https://lammapp.sa/developers/lamma-js) and [lamma.py](https://lammapp.sa/developers/lamma-py), shaped like discord.js and discord.py | | Every app works through its own bot account, and Lamma's permission checks apply to it like to anyone else. An app only joins a server when someone who manages that server approves exactly what it asks for. ## Privacy comes first Some things are never offered to apps, because Lamma keeps them between people: - Direct messages, presence (online status and activity) and typing. - Servers the app isn't installed in, and channels its bot can't see. - Message text, unless the app is approved for it or the message mentions the bot. Read the [developer rules](https://lammapp.sa/developers/developer-rules) before you ship. ## Next steps - [Build your first bot](https://lammapp.sa/developers/quick-start) in a few minutes. - Coming from Discord? Follow [Migrate from discord.js](https://lammapp.sa/developers/migrate-discord-js) or [Migrate from discord.py](https://lammapp.sa/developers/migrate-discord-py). - Using an AI assistant? Every page has a **Copy page** menu, and [llms.txt](https://lammapp.sa/llms.txt) lists all pages for AI tools. --- # Build your first bot > Make an app, install it in a server and run a bot that answers a message and a slash command. This guide makes an app, installs it in a server you manage, and runs a bot that answers `!hello` and a `/ping` command. > [!NOTE] > You need developer access during the beta. Without it, User Settings › Developer says apps are in beta. Email [lamma.help@gmail.com](mailto:lamma.help@gmail.com) to ask for access. ## 1. Create an app 1. Open Lamma and go to **User Settings › Developer**. 2. Type a name (2 to 32 characters) and select **Create app**. 3. Open the app. Under **What installs ask for**, keep `bot`, `messages.send` and `commands`, and add the intents your bot needs. This guide uses **Server, channel and role changes**, **New, edited and deleted messages** and **The text of every message it can see** (`Guilds`, `GuildMessages` and `MessageContent` in discord.js). 4. Select **Save changes**. **The text of every message it can see** (`MessageContent`) is privileged. While you develop, it works in up to five servers you own. For other servers the app needs [review](https://lammapp.sa/developers/apps#privileged-access). ## 2. Get a token Under **Bot token**, select **Reset token** and copy it. It starts with `lmb1_` and is shown only once. > [!WARNING] > The token works like a password for your bot. Keep it out of your code and out of Git: put it in an environment variable such as `LAMMA_TOKEN`. If it leaks, reset it and select **Revoke all tokens**. ## 3. Install it in a server Copy the **Install link** (`https://lammapp.sa/app?app=`) and open it. Pick a server you manage, check what the app asks for, and approve. The bot joins with an **App** badge and its own role. ## 4. Run the bot The SDKs aren't published to npm or PyPI yet. Until they are, the same bot can talk to the [REST API](https://lammapp.sa/developers/api-reference) and [gateway](https://lammapp.sa/developers/gateway) directly from any language. With [lamma.js](https://lammapp.sa/developers/lamma-js): ```js import { Client, Events, GatewayIntentBits, SlashCommandBuilder, MessageFlags } from 'lamma.js'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); client.once(Events.ClientReady, async ready => { console.log(`Ready as ${ready.user.username}`); const ping = new SlashCommandBuilder().setName('ping').setDescription('Check the bot'); await ready.application.commands.set([ping]); }); client.on(Events.MessageCreate, async message => { if (!message.author.bot && message.content === '!hello') await message.reply(`Hello, ${message.author.displayName}!`); }); client.on(Events.InteractionCreate, async interaction => { if (interaction.isChatInputCommand() && interaction.commandName === 'ping') { await interaction.reply({ content: 'Pong!', flags: MessageFlags.Ephemeral }); } }); client.login(process.env.LAMMA_TOKEN); ``` The same bot with [lamma.py](https://lammapp.sa/developers/lamma-py): ```python import lamma from lamma.ext import commands intents = lamma.Intents.default() intents.message_content = True class MyBot(commands.Bot): async def setup_hook(self): await self.tree.sync() bot = MyBot(command_prefix='!', intents=intents) @bot.command() async def hello(ctx): await ctx.reply(f'Hello, {ctx.author.display_name}!') @bot.tree.command(description='Check the bot') async def ping(interaction: lamma.Interaction): await interaction.response.send_message('Pong!', ephemeral=True) bot.run() # reads LAMMA_TOKEN ``` Global commands show up in every server at once, so `/ping` is ready as soon as the bot starts. ## 5. Try it In a channel the bot can see, send `!hello`, then type `/ping`. The answer to `/ping` is ephemeral: only you see it. ## Without an SDK Any HTTP client and WebSocket library will do. Send the token on every request: ```bash curl https://lammapp.sa/v1/users/@me \ -H "Authorization: Bot $LAMMA_TOKEN" ``` Then follow [Gateway](https://lammapp.sa/developers/gateway) to receive events. --- # Apps and installs > Applications, bot accounts, installs, scopes, intents, permissions and privileged access. An **app** is something you build that runs on your own computer or server and talks to Lamma through its bot account. People add it to their servers with an install link. ## Applications You make apps in **User Settings › Developer**. Each app has: - A name (2 to 32 characters), a description (up to 400) and optional https links to a privacy policy and terms of service. People see these on the install screen. - A **bot account** named `app_`. It can't sign in to Lamma, has no password or passkey, and shows an **App** badge wherever its name appears. It never counts toward a server's member limit and can't be added as a friend. - **What installs ask for**: its scopes, gateway intents and bot permissions. You can have up to 25 apps. Deleting an app revokes every token and removes it from every server. Its old messages stay, shown as from "Deleted app". ## Installs An install puts the app in one server. Anyone with **Manage server** can install it from the install link: ``` https://lammapp.sa/app?app= ``` The link opens an install screen built by Lamma. It shows the app, its developer, whether it has been reviewed, and each scope, intent and permission it asks for, with a plain list of what the developer will receive. Permissions the person installing doesn't have themselves can't be granted. If the app changes what it asks for while the screen is open, the install is refused and a fresh screen is shown, so people always approve what the app really gets. Installing adds the bot to the server with a **managed role** holding exactly the approved permissions. Server managers can rename and move that role or take permissions away, but never add more than the install granted. An install ends when someone removes the app in **Server settings › Apps**, kicks or bans the bot, or deletes the server, or when you delete the app. Every install and removal is written to the server's audit log. Apps can't be installed in Lamma's own community server. ## Scopes Scopes say which parts of the [REST API](https://lammapp.sa/developers/api-reference) the app may use. | Scope | Allows | | --- | --- | | `bot` | The bot account itself. Always required | | `messages.send` | Sending, editing and deleting its own messages | | `messages.read` | Reading message history | | `reactions` | Adding and removing its own reactions | | `commands` | [Slash commands](https://lammapp.sa/developers/commands). Asked for by default | | `members.read` | Listing a server's members. Needs the `guild_members` intent | A call outside the install's scopes answers `403` with `{"error": "missing_scope", "scope": ""}`. ## Gateway intents Intents say which [events](https://lammapp.sa/developers/events) the app receives. They are names, not a bitfield. | Intent | Events | | --- | --- | | `guilds` | Server, role and channel changes | | `guild_messages` | Messages sent, edited and deleted | | `message_content` | The text, embeds, attachments and polls inside message events. **Privileged** | | `guild_reactions` | Reactions added and removed | | `guild_members` | Members joining, changing and leaving. **Privileged** | | `guild_moderation` | Bans, timeouts and new audit log entries. Also needs the **View audit log** permission | | `guild_expressions` | Custom emoji changes | ## Bot permissions The bot's managed role can only hold permissions from this list: View channels, Send messages, Embed links, Attach files, Add reactions, Read message history, Mention everyone, Manage messages, Pin messages, Create polls, Bypass slowmode and View audit log. Administrator, server management, moderation and voice permissions aren't offered. Everything the bot does still goes through Lamma's permission checks as its own account: channel permissions, Read message history, timeouts and slowmode apply to it like to anyone else. Scopes and intents can only narrow that. ## Privileged access `message_content` and `guild_members` (and `members.read`, which follows it) are privileged. They work only: - once Lamma has **reviewed** your app for them. Ask from **User Settings › Developer** in the app's **Review** section, or - under the **development allowance**: in servers you own, up to five of them. Both are checked on every request and every message event, so if approval is withdrawn or a server changes owner, the access ends at once, even on a gateway connection that is already open. Without `message_content`, message events still carry the author, channel, time, mentions and reactions, but `content`, `embeds`, `attachments` and `poll` are empty and `content_available` is `false`. Messages that mention the bot directly, and messages the bot wrote, always include their text. ## Beta limits | Limit | Value | | --- | --- | | Apps per developer | 25 | | Servers per app before review | 25 | | Development allowance | 5 servers you own | | Installs | 10 an hour per person installing | --- # Tokens and authentication > How apps authenticate with bot tokens, and how to reset and revoke them. Apps authenticate with a **bot token**. Webhooks don't use tokens: their secret URL is the credential (see [Webhooks](https://lammapp.sa/developers/webhooks)). ## Bot tokens A token looks like this: ``` lmb1_1234567890123456789_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefg ``` Send it on every REST request in the `Authorization` header, with the word `Bot` in front: ```http GET /v1/users/@me HTTP/1.1 Host: lammapp.sa Authorization: Bot lmb1_… ``` On the gateway, send it in the `identify` message instead (see [Gateway](https://lammapp.sa/developers/gateway)). - Tokens work only on `/v1`. Lamma refuses a bot token everywhere else, and ignores sign-in cookies on `/v1`. - Lamma stores only a hash of the secret part, so it can't show a token again. Copy it when it is made. - A missing token answers `401 bot_token_required`, a wrong or revoked one `401 bot_token_invalid`, and a token for a switched-off or deleted app `403 application_disabled`. ## Resetting a token In **User Settings › Developer**, open the app and select **Reset token**. For safety, this needs a sign-in within the last ten minutes. At most two tokens are live at once. After a reset, the previous token keeps working for one hour, so a running bot can switch over without going offline. To cut off every token at once, select **Revoke all tokens**: open gateway connections close within ten seconds. > [!WARNING] > Never put a token in code you share, in a Git repository, in a screenshot or in a chat. Load it from an environment variable or a secrets manager. If you think it leaked, revoke all tokens straight away. ## Session cookies and the product API Lamma's own app uses routes under `/api/` with a sign-in cookie. Those routes are for people, not bots: they aren't part of this API and can change at any time. The only `/api/` routes meant for developers are the [webhook URLs](https://lammapp.sa/developers/webhooks). --- # Rate limits > Limits on the REST API, gateway, webhooks, slash commands and installs. Rate limits keep Lamma fast for everyone. Each limit below is counted separately, and the server's own slowmode still applies on top unless the bot has **Bypass slowmode**. ## REST API | Limit | Value | | --- | --- | | Requests per token | 50 a second | | Messages per bot per channel | 5 every 5 seconds | | Request body | 64 KB | Every `/v1` response carries `RateLimit` and `RateLimit-Policy` headers (IETF draft 8), so you can slow down before you hit a limit. Going over answers: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json { "error": "rate_limit", "retry_after": 1250 } ``` > [!NOTE] > On `/v1`, `retry_after` is in **milliseconds**. The SDKs wait and retry for you, up to three times by default. ## Gateway | Limit | Value | | --- | --- | | Connections per bot | 1 (a new one replaces the old) | | Connection attempts | 30 a minute per address | | Identify | 2 every 5 seconds per token, and 20 a minute per address | | Message from the client | 8 KB | Going over the identify limit closes the connection with code `4008`. Wait a few seconds before you connect again, and use backoff that grows on each failure. ## Webhooks | Limit | Value | | --- | --- | | Per webhook | 5 requests every 2 seconds | | Per channel | 30 a minute | | Request body | 64 KB | Only requests with a valid secret count, so nobody without the URL can use up its allowance. Going over answers `429` with `retry_after` in **seconds** (as on Discord) and a `Retry-After` header. ## Slash commands People can use commands 10 times every 10 seconds each. Registering commands counts toward the normal REST limit. ## Installs Each person can install apps 10 times an hour. --- # Slash commands > Register slash commands, receive them, and answer within three seconds. Slash commands let people use your app by typing `/` at the start of the message box. They need the `commands` scope on the install, which new apps ask for by default. ## Registering commands Commands use the same JSON shape as Discord's, so command builders from discord.js and discord.py produce valid input. Keys Lamma doesn't use, such as localizations, are ignored. `PUT /v1/applications/@me/commands` Replaces all of the app's global commands. A command keeps its id as long as its name stays the same. Global commands show up in every server at once. `POST /v1/applications/@me/commands` Creates one command, or updates the one with the same name. Answers `201` when created and `200` when updated. `GET /v1/applications/@me/commands` Lists the global commands. `GET /v1/applications/@me/commands/{command_id}` One command. `PATCH` changes it and `DELETE` removes it. `PUT /v1/applications/@me/guilds/{guild_id}/commands` The same set of routes exists under `/guilds/{guild_id}/` for one server the app is installed in. A server command replaces a global command with the same name in that server. `@me` can also be the application's own id. ```json { "name": "roll", "description": "Roll dice", "options": [ { "type": 4, "name": "sides", "description": "How many sides", "required": true, "min_value": 2, "max_value": 100 }, { "type": 3, "name": "label", "description": "What for", "choices": [{ "name": "Luck", "value": "luck" }] } ] } ``` ### Option types | Type | Name | Extra fields | | --- | --- | --- | | `3` | String | `min_length`, `max_length`, `choices` | | `4` | Integer | `min_value`, `max_value`, `choices` | | `5` | Boolean | | | `6` | User | | | `7` | Channel | | | `8` | Role | | | `10` | Number | `min_value`, `max_value`, `choices` | Required options come first. Subcommands, autocomplete, attachment and mentionable options aren't supported yet. ### Limits - 100 commands globally, and 100 per server. - 25 options per command and 25 choices per option. - Names are 1 to 32 lowercase letters, digits, `-` or `_`. Descriptions are 1 to 100 characters. > [!WARNING] > `default_member_permissions` isn't enforced yet. Anyone who can post in the channel can use every command, so check the person's roles in your command code before doing anything that needs permission. ## Who sees a command People see an app's commands in a channel when the app is installed in that server with `commands`, its bot can see and post in the channel, and they can post there too. Someone who is timed out can't use commands. ## Receiving a command Lamma checks the options first: types, required fields, choices, ranges, and that each user, role or channel belongs to this server. Only then does it send `INTERACTION_CREATE` to the app's gateway connection, whatever intents it asked for. If the app isn't connected, the person is told at once. ```json { "id": "…", "application_id": "…", "type": 2, "guild_id": "…", "channel_id": "…", "user": { "id": "…", "username": "noura", "name": "Noura", "avatar_id": null, "bot": false }, "member": { "nickname": null, "roles": ["…"], "permissions": ["view_channels", "send_messages"] }, "data": { "id": "…", "name": "roll", "type": 1, "options": [{ "name": "sides", "type": 4, "value": 20 }], "resolved": { "users": {}, "members": {}, "roles": {}, "channels": {} } }, "app_permissions": ["view_channels", "send_messages"], "created_at": 1759312800000 } ``` `member.permissions` is what the person who used the command may do in this channel, with roles and channel overrides, and `app_permissions` is the same for the bot. Check `member.permissions` in commands only some people should use. The SDKs give both as Discord's permission bitfields. ## Answering Answer every interaction within **3 seconds**, either with the answer or with a "thinking" state you fill in later. `POST /v1/interactions/{interaction_id}/callback` Once per interaction. Type `4` answers; type `5` shows that the app is thinking. ```json { "type": 4, "data": { "content": "You rolled 17", "flags": 64 } } ``` `PATCH /v1/interactions/{interaction_id}/messages/@original` For 15 minutes: posts the answer after a type `5`, or edits it. `POST /v1/interactions/{interaction_id}/followups` For 15 minutes: sends another message. - These calls use the bot token like every other `/v1` call. There is no separate interaction token. - The answer is a message from the bot, under its own permissions, with `interaction: { id, name, user_id }`, so people see who used which command above it. Slowmode doesn't hold it back. - `flags: 64` makes the answer **ephemeral**: only the person who used the command sees it, and it is never stored. It disappears when they dismiss it or reload. - No answer within 3 seconds tells the person the app didn't respond. A late callback answers `400 interaction_already_handled`. - Each call is checked again when it is made. If the app was removed from the server or lost sight of the channel, the call answers `404`. --- # Webhooks > Post into a channel from your own service with a secret URL. A webhook is a secret URL that posts messages into one server text channel, without signing in and without a bot. It suits alerts, build results, game scores and anything else your own service wants to announce. The request body follows Discord's "Execute Webhook" for the parts Lamma supports, so a script written for a Discord webhook usually works once you swap the URL. ## Making a webhook People with the **Manage webhooks** permission in a channel make its webhooks in **Server settings › Integrations**. Apps can't create webhooks. The URL looks like this: ``` https://lammapp.sa/api/webhooks/{webhook_id}/{secret} ``` It is shown once, when the webhook is made or given a new URL. Lamma keeps only a hash of the secret, so it can't show the URL again. > [!WARNING] > The URL is the only credential. Anyone who has it can post until someone makes a new URL. Keep it out of public code and logs. A webhook can post only as far as the person who made its current URL could. If they leave the server, the webhook stops for good (`403`, `webhook_paused`, `paused_reason: "maker_left"`) until someone makes a new URL. While they are timed out or can't send messages in the channel, it is paused (`paused_reason: "maker_cannot_send"`) and works again as soon as they can. ## Executing a webhook `POST /api/webhooks/{webhook_id}/{secret}` Send JSON with `Content-Type: application/json`. No cookie or token is needed. ```bash curl -X POST "https://lammapp.sa/api/webhooks//?wait=true" \ -H "Content-Type: application/json" \ -d '{"content": "Build 42 passed", "embeds": [{"title": "Release", "description": "Version 2.1 is live", "color": 5164938}]}' ``` Answers `204 No Content`. With `?wait=true` it answers `200` with the message: `id`, `channel_id`, `guild_id`, `content`, `author`, `embeds`, `timestamp` and `webhook_id`. | Field | Notes | | --- | --- | | `content` | Up to 2,000 characters. Mentions use `<@user id>`, `<@&role id>`, `@username` and `@everyone`. `<#channel id>` links a channel | | `username` | Overrides the webhook's name for this post | | `embeds` | Up to 10, with Discord's limits: `title` 256, `description` 4,096, `url`, `color` (a number), `timestamp`, `author.name` and `author.url`, `footer.text`, up to 25 `fields`, 6,000 characters in total | | `cards` | Up to 5 [link cards](https://lammapp.sa/developers/link-cards), a Lamma feature. Pictures in webhook cards aren't supported yet | | `allowed_mentions` | `parse` (`users`, `roles`, `everyone`), `users`, `roles`. Without it, mentions notify as far as the webhook's maker could mention in that channel | ### Accepted but not shown `avatar_url`, embed `image`, `thumbnail`, `video`, `provider`, `author.icon_url` and `footer.icon_url`. Lamma never loads pictures from other sites into people's browsers, so the webhook's own picture is used instead. ### Ignored `tts`, `flags`, `thread_id`, `thread_name`, `components`, `poll` and `applied_tags`. ### Not supported File uploads (`multipart/form-data`) answer `415`. Editing or deleting messages through the URL, changing the webhook through the URL, and the `/slack` and `/github` variants answer `404` or `405`. ## Getting a webhook `GET /api/webhooks/{webhook_id}/{secret}` Returns the webhook's id, name, channel and server. Never the secret or who made it. ## Errors | Status | Code | Meaning | | --- | --- | --- | | `400` | `50035` | The body is invalid | | `400` | `50006` | The message is empty | | `403` | `200000` | AutoMod blocked the post | | `403` | | `webhook_paused`, see above | | `404` | `10015` | Unknown webhook. Also for a wrong secret or a deleted channel, so the URL never says which part was wrong | | `415` | `50109` | The body isn't JSON | | `429` | | Rate limited. See [Rate limits](https://lammapp.sa/developers/rate-limits#webhooks) | ## How posts appear A post shows the webhook's name (or the `username` you sent) and picture with a **Webhook** tag. Nobody can edit it, and deleting it needs Manage messages. AutoMod checks webhook posts, including the text of embeds and cards, unless a rule skips webhooks. --- # Link cards > Design how your website's links look when someone shares them in Lamma. A link card is a small layout built from a fixed set of pieces. Websites use one to design how their links look when someone shares them in Lamma, and [webhooks](https://lammapp.sa/developers/webhooks) can post them. Cards can only show text, pictures and link buttons: nothing on a card runs code in Lamma. ## Adding a card to your website Put one ` ``` Lamma reads the first such script when it fetches your page for a link preview. Relative links and pictures resolve against the page's URL. If the page has no card, or the card breaks a rule below, the link gets a normal preview from its Open Graph tags, so keep those too. Cards refresh with previews, every few hours. Pages that already declare a Discord component embed get a card without changes. If a page has both, Lamma's own card wins. ## Pieces | `type` | Fields | Shows | | --- | --- | --- | | `text` | `text` | Formatted text, with the same Markdown as messages: headings `#` to `###`, bold, italics, underline, strikethrough, spoilers, code, quotes, lists and plain links. No HTML | | `section` | `text`, `aside` | Text with a picture (`{ "type": "image", "src", "alt", "spoiler" }`) or a link button (`{ "type": "link", "label", "url" }`) beside it | | `gallery` | `images`: 1 to 6 of `{ "src", "alt", "spoiler" }` | A grid of pictures that open in Lamma's image viewer | | `divider` | `line` (default `true`), `space` (`small` or `large`) | A line, or just space | | `links` | `links`: 1 to 5 of `{ "label", "url" }` | A row of link buttons | `accent` (`#rgb` or `#rrggbb`) colours the card's edge. ## Limits - The JSON is at most 16 KB, with 1 to 20 items, and not only dividers. - Each `text` is 1 to 2,000 characters, with at most 4,000 characters of text and button labels in the whole card. - Button labels are up to 80 characters, and alt text up to 300. - Button links are `http` or `https`. Pictures must be `https`, with at most 6 different pictures per card. - An unknown `type`, a wrong field type or a broken limit makes the whole card invalid. A website falls back to its normal preview, and a webhook gets `400`. ## How Lamma shows cards - Every button opens through Lamma's leaving-Lamma warning, like links in messages. - Website cards are labelled "Website card" with the hostname of the shared link, and each button shows the hostname it leads to, so a page can't pass itself off as Lamma or another site. - Pictures are fetched once by Lamma (PNG, JPEG, WebP or GIF, up to 1 MB each) and served from Lamma, never loaded from your site by people's browsers. - AutoMod checks the text, labels, links and alt text on cards. A card that breaks a server's rules isn't shown there, and the link keeps its normal preview. --- # Gateway > Receive events over a WebSocket: connecting, heartbeats, intents, reconnecting and close codes. The gateway is a WebSocket that pushes [events](https://lammapp.sa/developers/events) to your bot as they happen: new messages, members joining, slash commands being used. Lamma decides who may see each event once, for people and bots together, so a bot never hears about anything its account couldn't see. ## Connecting ``` wss://lammapp.sa/v1/gateway ``` You can also ask for the address with `GET /v1/gateway`, which returns `{ "url": "wss://…" }`. Messages are JSON text frames. Every message has an `op`, and most have a `d` with its data. 1. Open the WebSocket. Lamma sends `hello`: ```json { "op": "hello", "d": { "heartbeat_interval": 30000 } } ``` 2. Within 10 seconds, send `identify` with your token and the intents you want: ```json { "op": "identify", "d": { "token": "lmb1_…", "intents": ["guilds", "guild_messages"] } } ``` Send it once. A second identify on the same connection closes it with `4005`. 3. Lamma sends a `READY` event, then one `GUILD_CREATE` for each server the app is installed in. 4. Every `heartbeat_interval` milliseconds, send a heartbeat. Lamma answers with `heartbeat_ack`. Missing two intervals closes the connection. ```json { "op": "heartbeat" } ``` Events then arrive as `dispatch` messages: ```json { "op": "dispatch", "t": "MESSAGE_CREATE", "s": 12, "d": { "id": "…", "content": "hi" } } ``` `t` is the event name, `s` counts up by one on each event on this connection, and `d` is the event's data. ## Ops | Op | Sent by | Meaning | | --- | --- | --- | | `hello` | Lamma | First message. Carries `heartbeat_interval` in milliseconds | | `identify` | Your bot | Token and intents. Once per connection | | `heartbeat` | Your bot | Keeps the connection open | | `heartbeat_ack` | Lamma | Answer to a heartbeat | | `dispatch` | Lamma | An event | ## Intents A connection receives the intents it asked for, within what each server's install granted. Privileged intents count only where [privileged access](https://lammapp.sa/developers/apps#privileged-access) holds. The list each server allows is in its `GUILD_CREATE` and `GUILD_UPDATE`. Asking for an intent name Lamma doesn't know closes the connection with `4013`. Some events are always sent, whatever the intents: `READY`, `GUILD_CREATE`, `GUILD_DELETE` and `INTERACTION_CREATE`. See [Gateway events](https://lammapp.sa/developers/events) for the full list. ## Reconnecting Each bot has one connection. Connecting again replaces the older connection, which closes with `4009`. There is no resume. When a connection drops, wait, connect again, identify again, and fetch anything you missed over the REST API, for example with `GET /v1/channels/{channel_id}/messages?after=`. Wait longer after each failed attempt, starting at about one second. Events are delivered at most once per connection. The token is checked again every 10 seconds, so a revoked token or a deleted app ends the connection even if nothing else happens. A bot that falls more than 1 MB behind on reading is closed with `4009`. ## Close codes | Code | Meaning | Reconnect? | | --- | --- | --- | | `4000` | Unknown error | Yes | | `4001` | Unknown op | Fix the client first | | `4002` | Invalid JSON | Fix the client first | | `4003` | Didn't identify within 10 seconds | Yes | | `4004` | Token invalid or revoked, or the app was switched off or deleted | No | | `4005` | Already identified on this connection | Fix the client first | | `4008` | Identify rate limited | Yes, after a pause | | `4009` | Missed heartbeats, read too slowly, or replaced by a newer connection | Yes | | `4013` | Invalid intents | Fix the client first | ## Never sent to bots Presence, typing and direct messages are never sent to apps, and nothing is sent from servers the app isn't installed in. --- # Gateway events > Every event the gateway sends, and the intent it needs. Events arrive over the [gateway](https://lammapp.sa/developers/gateway) as `dispatch` messages. Each one carries the name in `t` and its data in `d`. Objects inside events have the same shape as in the [REST API](https://lammapp.sa/developers/api-reference#objects). ## Events by intent | Intent | Events | | --- | --- | | Always sent | `READY`, `GUILD_CREATE`, `GUILD_DELETE`, `INTERACTION_CREATE` | | `guilds` | `GUILD_UPDATE`, `GUILD_ROLE_CREATE`, `GUILD_ROLE_UPDATE`, `GUILD_ROLE_DELETE`, `CHANNEL_CREATE`, `CHANNEL_UPDATE`, `CHANNEL_DELETE` | | `guild_messages` | `MESSAGE_CREATE`, `MESSAGE_UPDATE`, `MESSAGE_DELETE`, `MESSAGE_DELETE_BULK` | | `message_content` | Fills in the text, embeds, attachments and poll of message events | | `guild_reactions` | `MESSAGE_REACTION_ADD`, `MESSAGE_REACTION_REMOVE` | | `guild_members` | `GUILD_MEMBER_ADD`, `GUILD_MEMBER_UPDATE`, `GUILD_MEMBER_REMOVE` | | `guild_moderation` | `GUILD_BAN_ADD`, `GUILD_BAN_REMOVE`, `GUILD_AUDIT_LOG_ENTRY_CREATE`, and timeouts in member events | | `guild_expressions` | `GUILD_EMOJIS_UPDATE` | ## Connection ### READY Sent once after `identify`. ```json { "v": 1, "session_id": "…", "user": { "id": "…", "username": "app_…", "name": "My bot", "avatar_id": null, "bot": true }, "application": { "id": "…", "name": "My bot" }, "intents": ["guilds", "guild_messages"], "guilds": [{ "id": "…", "unavailable": true }] } ``` `intents` is what the connection asked for. What each server allows is in its `GUILD_CREATE`. ## Servers ### GUILD_CREATE Sent for each server after `READY`, and when the app is installed while connected. `d` is the [server snapshot](https://lammapp.sa/developers/servers#server-snapshot) plus `scopes` and `intents` for this server and `emojis` (`[{ id, name, animated, available, image_id }]`). ### GUILD_UPDATE The server's settings, channels or roles changed. Same shape as `GUILD_CREATE`. Changes close together are sent as one update, at most every 300 ms. ### GUILD_DELETE `{ "id": "…" }`. The app was removed from the server: uninstalled, its bot kicked or banned, or the server deleted. ## Messages ### MESSAGE_CREATE and MESSAGE_UPDATE The [message object](https://lammapp.sa/developers/channels#message-object). Without `message_content`, the text is empty unless the message mentions the bot or the bot wrote it. ### MESSAGE_DELETE `{ "id", "channel_id", "guild_id" }` ### MESSAGE_DELETE_BULK `{ "ids", "channel_id", "guild_id" }`. Only ids of messages the bot could see. ### MESSAGE_REACTION_ADD and MESSAGE_REACTION_REMOVE `{ "channel_id", "guild_id", "message_id", "user_id", "emoji" }` ## Members ### GUILD_MEMBER_ADD and GUILD_MEMBER_UPDATE `{ "guild_id", "user", "nickname", "joined_at", "roles" }`, the [member object](https://lammapp.sa/developers/servers#member-object). Joining by invite and app installs are adds; nickname and role changes are updates. With [moderation access](#moderation-access) the member also has `communication_disabled_until`: when their timeout ends, in Unix milliseconds, or `null`. Starting or ending a timeout then sends an update. Without moderation access, timeouts send nothing. ### GUILD_MEMBER_REMOVE `{ "guild_id", "user" }`. Leaving, kicks, bans, prunes and app removals. Without moderation access, a kick or ban looks like any other leave. ## Moderation ### GUILD_BAN_ADD and GUILD_BAN_REMOVE `{ "guild_id", "user" }` ### GUILD_AUDIT_LOG_ENTRY_CREATE The same entry as [`GET /guilds/{guild_id}/audit-logs`](https://lammapp.sa/developers/audit-logs): ```json { "id": "…", "guild_id": "…", "action_type": 22, "action": "member_banned", "user_id": "…", "target_id": "…", "reason": "Spam", "options": {}, "changes": [], "created_at": 1759312800000 } ``` ### Moderation access Moderation events need the `guild_moderation` intent **and** the View audit log permission (or Administrator) on the bot's role in that server, checked on every event. In Lamma only people who can open the audit log see timeouts, bans and who did what, so bots need the same. ## Roles and channels ### GUILD_ROLE_CREATE and GUILD_ROLE_UPDATE `{ "guild_id", "role" }`. Reordering roles sends an update for every role. ### GUILD_ROLE_DELETE `{ "guild_id", "role_id" }` ### CHANNEL_CREATE and CHANNEL_UPDATE The [channel object](https://lammapp.sa/developers/channels#channel-object), only for channels the bot can see. When a channel changes so the bot can no longer see it, the bot gets `CHANNEL_DELETE`; when it changes so the bot can now see it, `CHANNEL_CREATE`. ### CHANNEL_DELETE `{ "id", "guild_id" }` ## Emoji ### GUILD_EMOJIS_UPDATE `{ "guild_id", "emojis" }`, the server's whole list of custom emoji. ## Commands ### INTERACTION_CREATE Someone used one of the app's slash commands. See [Slash commands](https://lammapp.sa/developers/commands#receiving-a-command). ## Order and delivery Server events come from the server's audit log, after each change is saved, in order within each server. Access is checked when the event goes out: if the bot loses sight of a channel or the View audit log permission first, it doesn't get the restricted parts. A change that was undone before it was saved sends nothing. --- # API reference > Base URL, versioning, conventions, errors and objects of the REST API. The REST API is how apps read and change things in Lamma. It is versioned, JSON in and out, and shaped like Discord's so code ported from there reads the same. ## Base URL ``` https://lammapp.sa/v1 ``` Every request needs a [bot token](https://lammapp.sa/developers/authentication) in the `Authorization` header. Request bodies are JSON, up to 64 KB. Responses are never cached (`Cache-Control: no-store`). ## Versioning `/v1` only grows: new fields, endpoints and error codes can appear, but nothing is removed or changes meaning. Ignore fields you don't know. Anything that would break existing bots goes into a new `/v2`, with notes on how to move. ## Conventions - **IDs** are strings. They are Lamma's own ids, so a Discord id won't find anything. - **Times** are Unix time in milliseconds, unless a field says otherwise. - **Servers** are called `guilds` in paths and fields, as on Discord. - **Responses** are the object or list itself, with no wrapper. - **Path parameters** are written `{name}` on these pages. `@me` means the bot or its application. ## Errors Errors have a status and a short code: ```json { "error": "missing_scope", "scope": "messages.read" } ``` | Status | Code | Meaning | | --- | --- | --- | | `400` | `message_invalid` and others | The request body is wrong | | `401` | `bot_token_required` | No token was sent | | `401` | `bot_token_invalid` | The token is wrong or revoked | | `403` | `application_disabled` | The app was switched off or deleted | | `403` | `missing_scope` | The install doesn't include a scope this call needs. `scope` names it | | `403` | `missing_permissions` | The bot's role lacks a permission this call needs | | `404` | `not_found` | It doesn't exist, or the bot can't see it | | `409` | `nonce_conflict` | The `nonce` was used before with a different message | | `429` | `rate_limit` | Slow down. See [Rate limits](https://lammapp.sa/developers/rate-limits) | Anything the bot can't see answers `404` exactly like something that doesn't exist: a server the app isn't in, a hidden or deleted channel, or a direct message. That way the API never confirms what is there. ## Objects ### User object | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `username` | string | Unique handle | | `name` | string | Display name | | `avatar_id` | string or null | | | `bot` | boolean | `true` for app bot accounts | The [server](https://lammapp.sa/developers/servers), [member](https://lammapp.sa/developers/servers#member-object), [role](https://lammapp.sa/developers/servers#role-object), [channel](https://lammapp.sa/developers/channels#channel-object) and [message](https://lammapp.sa/developers/channels#message-object) objects are described with their endpoints. ## Endpoints | Area | Page | | --- | --- | | The bot, its application and the gateway address | [Users and applications](https://lammapp.sa/developers/users) | | Servers, roles and members | [Servers](https://lammapp.sa/developers/servers) | | Channels, messages and reactions | [Channels and messages](https://lammapp.sa/developers/channels) | | Audit logs | [Audit logs](https://lammapp.sa/developers/audit-logs) | | Slash commands and interaction answers | [Slash commands](https://lammapp.sa/developers/commands) | --- # Users and applications > Get the bot, its application, its servers and the gateway address. Endpoints about the bot itself, its application, and where to connect. ## Get current bot `GET /v1/users/@me` Returns the bot's [user object](https://lammapp.sa/developers/api-reference#user-object). ```json { "id": "1234567890123456789", "username": "app_1234567890123456789", "name": "Scorekeeper", "avatar_id": null, "bot": true } ``` ## Get current application `GET /v1/applications/@me` Returns the app the token belongs to. | Field | Type | Notes | | --- | --- | --- | | `id` | string | The application id, used in install links | | `name` | string | | | `description` | string | | | `review_state` | string | Whether Lamma has reviewed the app | | `bot_user_id` | string | The bot account's user id | ## Get current bot servers `GET /v1/users/@me/guilds` The servers the app is installed in. ```json [{ "id": "…", "name": "Weekend squad", "icon_id": null }] ``` ## Get gateway `GET /v1/gateway` The address to open the [gateway](https://lammapp.sa/developers/gateway) WebSocket on. ```json { "url": "wss://lammapp.sa/v1/gateway" } ``` Lamma users can't be looked up by id or name through the API. A bot learns about people from the servers it is in: members, message authors and command users. --- # Servers > Read servers, roles and members. Servers are called guilds in the API, as on Discord. A bot can only read servers its app is installed in. ## Get server `GET /v1/guilds/{guild_id}` Returns the [server snapshot](#server-snapshot), with this install's effective `scopes` and `intents`. ## Get server channels `GET /v1/guilds/{guild_id}/channels` The [channels](https://lammapp.sa/developers/channels#channel-object) the bot can see. ## Get server roles `GET /v1/guilds/{guild_id}/roles` The server's [roles](#role-object), highest first. ## List server members `GET /v1/guilds/{guild_id}/members` Needs the `members.read` scope, which follows the privileged `guild_members` intent. Members come in user id order. | Query | Type | Notes | | --- | --- | --- | | `limit` | integer | 1 to 1,000 | | `after` | user id | Start after this user, to page through | ## Get server member `GET /v1/guilds/{guild_id}/members/{user_id}` One [member](#member-object). Needs `members.read`. ## Server snapshot | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `name` | string | | | `description` | string or null | | | `icon_id` | string or null | | | `owner_id` | string | | | `channels` | array | [Channels](https://lammapp.sa/developers/channels#channel-object) the bot can see | | `categories` | array | Only categories holding a channel the bot can see | | `roles` | array | [Roles](#role-object) | | `me` | object | `{ roles, permissions }`: the bot's own roles and server permissions | ## Role object | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `name` | string | | | `color` | string or null | | | `position` | integer | Higher is more important | | `permissions` | array of strings | Permission names, such as `send_messages` | | `hoist` | boolean | Shown separately in the member list | | `mentionable` | boolean | | | `managed` | boolean | `true` for an app's role, which only installs can change | | `default` | boolean | `true` for @everyone | ## Member object | Field | Type | Notes | | --- | --- | --- | | `user` | object | The [user object](https://lammapp.sa/developers/api-reference#user-object) | | `nickname` | string or null | | | `joined_at` | integer | Unix milliseconds | | `roles` | array of ids | | A member never includes presence, profile details or which other servers the person is in. --- # Channels and messages > Read channels and messages, send and edit messages, and add reactions. Read channels and messages, send and edit the bot's own messages, and add reactions. ## Get channel `GET /v1/channels/{channel_id}` A [channel](#channel-object) the bot can see. ## Get channel messages `GET /v1/channels/{channel_id}/messages` Needs `messages.read`. Returns [messages](#message-object), newest first. | Query | Type | Notes | | --- | --- | --- | | `limit` | integer | 1 to 100 | | `before` | message id | Messages before this one | | `after` | message id | Messages after this one | History follows the **Read message history** permission, as it does for people. Without it the bot only reads what was sent while it was connected, and the response has the header `X-History-Hidden: 1`. ## Get channel message `GET /v1/channels/{channel_id}/messages/{message_id}` One message. Needs `messages.read`. ## Create message `POST /v1/channels/{channel_id}/messages` Needs `messages.send` and the Send messages permission. Answers `201` with the message. ```json { "content": "Team A leads 3 to 1", "reply_to": "", "nonce": "score-update-0042", "embeds": [{ "title": "Score", "description": "Team A leads", "url": "https://example.com/match/42", "color": "#3355ff", "fields": [{ "name": "Team A", "value": "3", "inline": true }, { "name": "Team B", "value": "1", "inline": true }], "footer": { "text": "Final round" }, "timestamp": "2026-10-01T10:00:00Z" }], "allowed_mentions": { "parse": ["users"] } } ``` | Field | Notes | | --- | --- | | `content` | Up to 2,000 characters. Send `content`, at least one embed, or both | | `reply_to` | The id of the message to reply to | | `nonce` | 16 to 64 letters, digits or `-`. Sending the same nonce and message again returns the first message instead of posting twice. The same nonce with a different message answers `409 nonce_conflict` | | `embeds` | Up to 10. Needs Embed links | | `allowed_mentions` | Which mentions notify anyone, as on Discord: `parse`, `users`, `roles` (up to 100 ids each). Without it, every mention the bot is allowed to make notifies | ### Embed limits Title 256 characters, description 4,096, up to 25 fields (name 256, value 1,024), footer 2,048, author name 256, and 6,000 characters in total. `color` is `#rrggbb` or a number. Links must be `http` or `https`. Images, thumbnails and video aren't supported yet and are dropped. Embed text shows as plain text, and links go through Lamma's leaving-Lamma warning. ### Mentions Use `<@user id>`, `<@&role id>`, `@everyone` and `@here`, with the same permission checks as for people. ## Edit message `PATCH /v1/channels/{channel_id}/messages/{message_id}` Edits one of the bot's own messages. Needs `messages.send`. Takes the same fields as creating one. ## Delete message `DELETE /v1/channels/{channel_id}/messages/{message_id}` Deletes one of the bot's own messages and answers `204`. Bots can't delete other people's messages in v1. ## Add reaction `PUT /v1/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me` Needs `reactions`. Answers `204`. `{emoji}` is a URL-encoded emoji, such as `%F0%9F%91%8D` for 👍. ## Remove reaction `DELETE /v1/channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me` Removes the bot's own reaction. Answers `204`. ## Channel object | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `guild_id` | string | | | `name` | string | | | `kind` | string | `text`, `voice` and others. Not a number as on Discord | | `topic` | string or null | | | `position` | integer | | | `category_id` | string or null | | | `slowmode_seconds` | integer | | | `permissions` | array of strings | What the bot may do in this channel | ## Message object | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `channel_id` | string | | | `guild_id` | string | | | `author` | object | A [user object](https://lammapp.sa/developers/api-reference#user-object) | | `content` | string | Empty when `content_available` is `false` | | `content_available` | boolean | `false` when the text was withheld because the app lacks `message_content` | | `embeds` | array | | | `attachments` | array | | | `poll` | object or null | | | `mentions` | object | `{ users, roles, everyone }` | | `reactions` | array | `[{ emoji, count, me }]` | | `reply_to` | string or null | | | `interaction` | object or null | `{ id, name, user_id }` when the message answers a slash command | | `pinned` | boolean | | | `created_at` | integer | Unix milliseconds | | `edited_at` | integer or null | Unix milliseconds | --- # Audit logs > Read who did what in a server, and why. The audit log records who did what in a server, and why. Bots can read it with the **View audit log** permission on their role, the same rule people follow. ## Get server audit log `GET /v1/guilds/{guild_id}/audit-logs` Returns `{ "audit_log_entries": [...] }`, newest first. Without View audit log it answers `403 missing_permissions`. | Query | Type | Notes | | --- | --- | --- | | `limit` | integer | 1 to 100 | | `before` | entry id | Entries before this one | | `after` | entry id | Entries after this one | | `user_id` | user id | Only actions by this person | | `action_type` | integer | Only this kind of action | To hear about new entries as they happen, use the `guild_moderation` intent and [`GUILD_AUDIT_LOG_ENTRY_CREATE`](https://lammapp.sa/developers/events#guild_audit_log_entry_create). ## Audit log entry ```json { "id": "…", "guild_id": "…", "action_type": 22, "action": "member_banned", "user_id": "…", "target_id": "…", "reason": "Spam links", "options": {}, "changes": [], "created_at": 1759312800000 } ``` | Field | Type | Notes | | --- | --- | --- | | `id` | string | | | `guild_id` | string | | | `action_type` | integer or null | Discord's audit log event number, so existing code can switch on it: `22` for a ban, `24` for a member update such as a timeout or nickname, `30` for a new role. `null` for actions Discord doesn't log, such as joins and leaves | | `action` | string | Lamma's own name for the action, such as `member_banned` or `member_joined` | | `user_id` | string or null | Who did it. `null` for automatic actions such as AutoMod | | `target_id` | string or null | What it was done to | | `reason` | string or null | | | `options` | object | Extra details recorded with the action | | `changes` | array | `[{ key, old_value, new_value }]` for edits that recorded before and after | | `created_at` | integer | Unix milliseconds | --- # lamma.js > Build Lamma bots in JavaScript with the patterns you know from discord.js. lamma.js lets you build Lamma bots in JavaScript with the names and patterns you know from discord.js. It runs on Lamma's own [REST API](https://lammapp.sa/developers/api-reference) and [gateway](https://lammapp.sa/developers/gateway) and never talks to Discord. > [!NOTE] > lamma.js isn't published to npm yet. This page describes it ahead of release. It needs Node.js 22.4 or newer and has no dependencies. ## Example ```js import { Client, Events, GatewayIntentBits, EmbedBuilder, SlashCommandBuilder, MessageFlags, Colors } from 'lamma.js'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent] }); const commands = [ new SlashCommandBuilder().setName('roll').setDescription('Roll a die') .addIntegerOption(option => option.setName('sides').setDescription('How many sides').setRequired(true).setMinValue(2).setMaxValue(100)) ]; client.once(Events.ClientReady, async ready => { console.log(`Logged in as ${ready.user.username} in ${ready.guilds.cache.size} servers`); await ready.application.commands.set(commands); // global commands show up everywhere at once }); client.on(Events.MessageCreate, async message => { if (message.author.bot || message.content !== '!ping') return; const embed = new EmbedBuilder().setTitle('Pong!').setDescription(`Gateway ping: ${client.ws.ping} ms`).setColor(Colors.Green); await message.reply({ embeds: [embed] }); }); client.on(Events.InteractionCreate, async interaction => { if (!interaction.isChatInputCommand() || interaction.commandName !== 'roll') return; const sides = interaction.options.getInteger('sides', true); await interaction.reply({ content: `You rolled ${1 + Math.floor(Math.random() * sides)}`, flags: MessageFlags.Ephemeral }); }); client.login(); // reads LAMMA_TOKEN ``` ## Options | Option | Default | Notes | | --- | --- | --- | | `baseURL` | `LAMMA_URL` | Another copy of Lamma to reach, such as a local one. Defaults to Lamma's own address | | `intents` | `[]` | Which events to receive | | `gatewayURL` | from `GET /v1/gateway`, or `LAMMA_GATEWAY_URL` | Set it when your bot reaches Lamma by another address | | `rest.retries` | `3` | How many times to retry after a rate limit | | `allowedMentions` | every mention allowed | The default for every message | | `messageCacheLimit` | `200` | Messages kept per channel, in memory only, for edit and delete events. `0` keeps none | ## Coming from discord.js Most bots port by changing the import and the token. | discord.js | lamma.js | | --- | --- | | `import { Client } from 'discord.js'` | `import { Client } from 'lamma.js'` | | `new Client({ intents })` | The same | | `GatewayIntentBits` (bits, arrays, flag names such as `'GuildMessages'`, `IntentsBitField`) | The same bits. Lamma sends `Guilds`, `GuildMembers`, `GuildModeration`, `GuildExpressions`, `GuildMessages`, `GuildMessageReactions` and `MessageContent`. Others are ignored with one warning | | `client.login(token)` | The same. Defaults to `LAMMA_TOKEN` | | `Events.ClientReady` (`'clientReady'`), `MessageCreate`, `InteractionCreate`, `GuildCreate`, `Raw`, … | The same names. The old `'ready'` isn't emitted; a listener on it warns once | | `messageReactionAdd` and `messageReactionRemove`: `(reaction, user, { type, burst })` | The same. `reaction.emoji` has `id`, `name` and `identifier`. A reaction on an uncached message comes as a partial, as with `Partials.Message`: check `reaction.partial` and call `reaction.fetch()` | | `guildMemberAdd`, `guildMemberUpdate`, `guildMemberRemove`, `guildBanAdd`, `guildBanRemove`, `guildAuditLogEntryCreate` | The same. Timeouts, bans and audit entries need [moderation access](https://lammapp.sa/developers/events#moderation-access) | | `roleCreate`, `channelCreate`, `emojiCreate` and their update and delete events | The same. Caches stay up to date | | `guild.fetchAuditLogs()`, `AuditLogEvent` | The same. Entries Discord has no number for have `action: null` and `entry.lammaAction` | | `message.reply()`, `edit()`, `delete()`, `react()`, `message.reactions.cache` | The same. `react()` takes a Unicode emoji or a server emoji (`GuildEmoji`, `'<:name:id>'`, `'name:id'` or its id) | | `ChannelType`, `channel.type`, `TextChannel`, `VoiceChannel`, `CategoryChannel`, `channel.parent`, `isTextBased()`, `isSendable()` | The same. Voice channels have a text chat, as on Discord | | `PermissionsBitField`, `PermissionFlagsBits`, `role.permissions`, `member.permissions`, `guild.members.me` | The same | | `interaction.appPermissions`, `interaction.memberPermissions` | The same: what the bot, and the person who ran the command, may do in that channel | | `channel.permissionsFor()`, `member.permissionsIn()` | The same for the bot itself. For anyone else they return `null` and warn once, because Lamma doesn't send other members' channel permissions | | `channel.send()`, `channel.messages.fetch()` | The same | | `guild.members.fetch()` | The same. Needs `members.read` | | `EmbedBuilder` | The same. Images and thumbnails aren't shown yet | | `SlashCommandBuilder` | The same, for string, integer, number, boolean, user, channel and role options, with the same `toJSON()` as @discordjs/builders | | A command handler: `{ data, execute }` files in `client.commands`, and a `deploy-commands.js` that calls `rest.put(Routes.applicationGuildCommands(clientId, guildId), { body })` | The same, so a bot laid out like the discord.js guide ports as it is | | `BitField`, `UserFlagsBitField`, `ChannelFlagsBitField`, `GuildMemberFlagsBitField`, `RoleFlagsBitField` and the other bitfields | The same classes and values. Lamma has no account badges or channel, member and role flags, so those fields are empty | | `client.application.commands.set(commands, guildId)` | The same. Global commands appear at once | | `interaction.reply()`, `deferReply()`, `editReply()`, `followUp()` | The same, within 3 seconds and 15 minutes | | `flags: MessageFlags.Ephemeral` | The same. Other message flags are dropped with one warning | | `InteractionType`, `ApplicationCommandType`, `InteractionContextType`, `Partials` | The same values, so ported imports work | | `interaction.options.getString()`, `getInteger()`, `getUser()`, `getChannel()`, `getRole()` | The same | | `Collection` | The same helpers: `find`, `filter`, `map`, `first`, … | | `REST` and `Routes` | The same idea, with Lamma's paths | ## What's different - `setDefaultMemberPermissions` has no effect yet and warns once, so check `interaction.memberPermissions` in your command code. - Lamma's Mention roles permission has no Discord flag, so permission bitfields leave it out. - `reply({ withResponse: true })` gives `undefined` with one warning, and `fetchReply()` rejects. `editReply()` returns the message. - There are no buttons, select menus, modals, autocomplete or subcommands yet. `addSubcommand` throws so you notice. - Bots can't send DMs, show a status or show typing. `user.send()` and `createDM()` reject with code `50007`, as Discord does for someone with DMs closed. `sendTyping()`, `setPresence()` and `setActivity()` do nothing. Each warns once. - Bots can't kick, ban or time out members yet. - When the connection drops, the client reconnects on its own. There is no resume, so read anything you missed with `channel.messages.fetch()`. - An ephemeral reply is never stored, so `editReply()` on one returns a plain object, not a `Message`. See [Coming from Discord](https://lammapp.sa/developers/from-discord) for everything that differs. --- # lamma.py > Build Lamma bots in Python with the patterns you know from discord.py. lamma.py lets you build Lamma bots in Python with the names and patterns you know from discord.py. It runs on Lamma's own [REST API](https://lammapp.sa/developers/api-reference) and [gateway](https://lammapp.sa/developers/gateway) and never talks to Discord. > [!NOTE] > lamma.py isn't published to PyPI yet. This page describes it ahead of release. It needs Python 3.10 or newer and `aiohttp`. ## Example ```python import lamma from lamma import app_commands from lamma.ext import commands intents = lamma.Intents.default() intents.message_content = True class MyBot(commands.Bot): async def setup_hook(self): await self.tree.sync() # global commands show up everywhere at once bot = MyBot(command_prefix='!', intents=intents) @bot.command() async def hello(ctx): await ctx.reply(f'Hello, {ctx.author.display_name}!') @bot.tree.command(description='Roll a die') @app_commands.describe(sides='How many sides') async def roll(interaction: lamma.Interaction, sides: app_commands.Range[int, 2, 100]): await interaction.response.send_message(f'You rolled a d{sides}', ephemeral=True) bot.run() # reads LAMMA_TOKEN ``` ## Options | Option | Default | Notes | | --- | --- | --- | | `base_url` | `LAMMA_URL` | Another copy of Lamma to reach, such as a local one. Defaults to Lamma's own address | | `intents` | required | Which events to receive | | `gateway_url` | from `GET /v1/gateway`, or `LAMMA_GATEWAY_URL` | Set it when your bot reaches Lamma by another address | | `max_messages` | `1000` | Messages kept, in memory only, for edit and delete events | | `allowed_mentions` | every mention allowed | The default for every message | ## Coming from discord.py Most bots port by changing `import discord` to `import lamma as discord`, `from discord.ext import commands` to `from lamma.ext import commands`, and setting `LAMMA_TOKEN`. The [migration guide](https://lammapp.sa/developers/migrate-discord-py) has the details. | discord.py | lamma.py | | --- | --- | | `discord.Client(intents=...)`, `commands.Bot(...)` | The same | | `Intents.default()`, `.message_content`, `.members`, `.guilds`, `.messages`, `.reactions`, `.moderation`, `.expressions` | The same. Other flags are accepted, ignored with one warning, and read as `False` | | Int IDs, `guild.get_channel(id)`, `discord.utils.get(..., id=...)` | The same. Lookups also take an ID as a string | | `TextChannel`, `VoiceChannel`, `CategoryChannel`, `channel.type`, `guild.text_channels`, `guild.voice_channels` | The same. Bots can post in a voice channel's text chat | | `discord.Permissions` and its presets, `role.permissions`, `member.guild_permissions`, `interaction.permissions`, `interaction.app_permissions` | The same | | `MessageFlags`, `PublicUserFlags`, `MemberFlags`, `ChannelFlags`, `RoleFlags`, `AttachmentFlags`, `EmbedFlags`, `SystemChannelFlags`, `ApplicationFlags`, `MemberCacheFlags`, `AppCommandContext` and the other flag classes | The same names and bits. Lamma has none of these features, so the flags on messages, users, members, channels and roles are always empty | | `app_commands.checks.has_permissions()`, `bot_has_permissions()` | The same, exact for the channel | | `commands.bot_has_permissions()`, `has_guild_permissions()`, `bot_has_guild_permissions()` | The same | | `commands.has_permissions()` | Uses the author's server-wide permissions, because Lamma doesn't send channel overrides. It warns once | | `channel.permissions_for(guild.me)` | The same for the bot itself. For anyone else it raises `ClientException` | | `client.run(token)`, `await client.start(token)` | The same. `run()` defaults to `LAMMA_TOKEN` | | `setup_hook`, `on_ready`, `on_message`, `on_message_edit`, `on_message_delete`, `on_raw_*`, `on_guild_join`, `on_guild_remove`, `on_interaction` | The same | | `on_member_join`, `on_member_update`, `on_member_remove`, `on_member_ban`, `on_member_unban`, `on_audit_log_entry_create` | The same. Timeouts, bans and audit entries need [moderation access](https://lammapp.sa/developers/events#moderation-access) | | `guild.audit_logs(...)` | The same. Entries Discord has no action for have `action` set to `None` and `entry.lamma_action` | | `channel.send()`, `message.reply()`, `edit()`, `delete()`, `add_reaction()` | The same | | `channel.history()`, `channel.fetch_message()` | The same | | `guild.fetch_members()`, `guild.fetch_member()` | The same. Needs `members.read` | | `discord.Embed` | The same. Images and thumbnails aren't shown yet | | `app_commands.CommandTree`, `@tree.command`, `describe`, `choices`, `Range`, `Literal` | The same | | Option types `str`, `int`, `float`, `bool`, `Member`, `User`, `TextChannel`, `VoiceChannel`, `Role` | The same | | `tree.sync()`, `tree.sync(guild=...)` | The same. Global commands appear at once | | `interaction.response.send_message(ephemeral=True)`, `defer()`, `edit_original_response()`, `followup.send()` | The same, within 3 seconds and 15 minutes | | `commands.Cog`, `Cog.listener()`, `@bot.command()`, `Context`, converters, checks | The same | | `await bot.load_extension('cogs.fun')` with `async def setup(bot)`, `unload_extension()`, `reload_extension()` | The same | | `discord.utils.get`, `find`, `format_dt`, `escape_markdown`, `escape_mentions` | The same | ## What's different - IDs are Lamma IDs, so a Discord ID won't find anything. - `channel.permissions_for()` only works for the bot itself. Lamma doesn't send channel overwrites to bots, so it raises `ClientException` for anyone else rather than guess. Use `interaction.permissions` for the person who ran a command, or `member.guild_permissions` for server-wide permissions. - Lamma's Mention roles permission has no Discord flag, so it isn't part of `Permissions`. - `default_permissions` and `guild_only` are accepted but have no effect yet, so check `interaction.permissions` in your command code. - Channel options aren't limited by type yet. If someone picks a voice channel for a `TextChannel` parameter, the command fails with `app_commands.TransformerError`. - There are no buttons, views, select menus, modals, autocomplete, context menus or subcommand groups yet. `app_commands.Group` and `commands.GroupCog` raise so you notice. - Bots can't upload files yet. `discord.File` works, but `file=` and `files=` are left out of the message with one warning. - Bots can't send DMs, show a status or show typing. `user.send()` raises `Forbidden` with code `50007`, as Discord does for someone with DMs closed. `typing()` and `change_presence()` do nothing. Each logs one warning. - Bots can't kick, ban or time out members yet. - `client.fetch_user()` only returns people the bot has already seen. - Reaction events are raw only: use `on_raw_reaction_add` and `on_raw_reaction_remove`. - When the connection drops, the client reconnects and `on_ready` runs again. There is no resume, so read anything you missed with `channel.history()`. See [Coming from Discord](https://lammapp.sa/developers/from-discord) for everything that differs. --- # Coming from Discord > What carries over from Discord's API, what differs, and what bots can't do in Lamma. Lamma keeps its own API, tokens and payloads, so a Discord token, gateway address or raw payload won't work as it is. The shapes and names are close on purpose, though, and the SDKs keep most bot code the same. For step-by-step guides with every code change, see [Migrate from discord.js](https://lammapp.sa/developers/migrate-discord-js) and [Migrate from discord.py](https://lammapp.sa/developers/migrate-discord-py). ## Switch in two lines With the SDKs, most bots change only the import and the token (`LAMMA_TOKEN`). Lamma's address is built in, so the rest of the code stays as it is. ```diff - import { Client, GatewayIntentBits } from 'discord.js'; + import { Client, GatewayIntentBits } from 'lamma.js'; ``` ```diff - import discord - from discord.ext import commands + import lamma as discord + from lamma.ext import commands ``` ## What carries over | Discord | Lamma | | --- | --- | | Application and bot user | Application and bot account, one bot per app | | `Authorization: Bot ` | The same header, with an `lmb1_` token that works only on `/v1` | | OAuth2 install link | `https://lammapp.sa/app?app=`, which opens Lamma's install screen. There is no OAuth2 redirect flow yet | | Gateway hello, identify, heartbeat and dispatch | The same steps, with named ops instead of numbers | | Intents bitfield | The SDKs take Discord's intents. On the raw gateway, `identify` takes names, such as `guild_messages`. `message_content` and `guild_members` are privileged, as on Discord | | Event names | The same: `MESSAGE_CREATE`, `GUILD_MEMBER_ADD`, `INTERACTION_CREATE`, … | | Message, reaction, server, member and audit log paths | The same paths under `/v1` | | Embeds | The same fields and limits. No images, thumbnails or video yet | | Mentions and `allowed_mentions` | The same | | Message `nonce` | The same, and it really prevents double posts | | Audit log `action_type` | Discord's numbers | | Slash command JSON and bulk overwrite | The same. Option types 3 to 8 and 10 | | Interaction callback types 4 and 5, edit original, follow-ups, ephemeral flag 64 | The same, using the bot token instead of an interaction token | | Execute Webhook | The same body for text and embeds | ## What's different | Discord | Lamma | | --- | --- | | Permission bitfields | The SDKs give Discord's bitfields. The raw API sends permission names, such as `send_messages` | | Channel `type` number | The SDKs give Discord's `type`. The raw API sends `kind`, such as `text` | | `retry_after` in seconds | Milliseconds on `/v1` (seconds on webhooks, as on Discord) | | Gateway resume | Not yet: reconnect and catch up over REST | | Sharding | Not needed: one connection per bot | | Global commands take up to an hour | Global commands appear at once | | `default_member_permissions` | Not enforced yet: check permissions in your code | ## Not offered to bots | Discord feature | Lamma | | --- | --- | | Direct messages, presence and typing | Never offered to bots, for privacy. The SDKs behave as if every person had DMs closed | | Kicking, banning, timing out and deleting other people's messages | Not in v1. Bots can still hear about them | | Buttons, select menus, modals, autocomplete and subcommands | Later | | Voice bots | Not planned for v1 | | Threads, forums, stages and stickers | Lamma doesn't have them yet | | Outgoing webhooks | Later | --- # Migrate from discord.js > Move a discord.js v14 bot to lamma.js step by step, with every change your code needs. This guide moves a bot written for current discord.js (14.27) to [lamma.js](https://lammapp.sa/developers/lamma-js) one step at a time. Most bots keep their structure, events and command handlers. What changes is the import, the address, the token, a few names, and any feature Lamma doesn't offer bots. > [!NOTE] > lamma.js isn't published to npm yet. You can read this guide and check your bot against it now, then switch once the package is released. ## The short version For most bots the whole change is the import and the token: ```diff - import { Client, GatewayIntentBits, Events, SlashCommandBuilder } from 'discord.js'; + import { Client, GatewayIntentBits, Events, SlashCommandBuilder } from 'lamma.js'; ``` ```bash LAMMA_TOKEN=lmb1_... node index.js ``` Your intents, events, command files and permission checks stay as they are. Lamma's address is built in, as Discord's is. `login()` with no token reads `LAMMA_TOKEN`, and `client.login(process.env.DISCORD_TOKEN)` works too if you put the Lamma token there. Once lamma.js is on npm, an install alias (`npm install discord.js@npm:lamma.js`) will leave even the import untouched. The steps below are for bots that use something Lamma doesn't have yet, and for checking a port line by line. ## 1. Check what your bot uses Search your code for the features below first. Each needs a change, or is switched off on Lamma. | If your bot uses | On Lamma | | --- | --- | | `user.send()`, `createDM()`, DM commands | Bots can't DM people. The call fails with code `50007`, like a person with DMs closed | | `GuildPresences`, `member.presence`, `presenceUpdate` | Never sent to bots. The intent reads as off, so code that checks it switches itself off | | `sendTyping()`, `TypingStart` | Does nothing, with one warning | | `setPresence()`, `setActivity()` | Does nothing for now, with one warning | | Buttons, select menus, modals (`ButtonBuilder`, `ActionRowBuilder`, `ModalBuilder`) | Not yet. Rewrite those flows as slash command options | | Subcommands, autocomplete, context menus, attachment options | Not yet. `addSubcommand` throws so you notice. Use separate commands | | Components V2 (`ContainerBuilder`, `TextDisplayBuilder`, …) | Not yet. Use embeds | | `files`, `AttachmentBuilder`, embed images and thumbnails | Not yet. Images are dropped and files aren't sent | | `ban()`, `kick()`, `timeout()`, `roles.add()`, deleting other people's messages | Not in v1. Bots can still hear about bans, kicks and timeouts | | Voice (`@discordjs/voice`) | Not offered to bots | | Threads, forums, stages, stickers | Lamma doesn't have them yet | | `permissionsFor()`, `permissionsIn()` for someone other than the bot | Returns `null` with one warning. See [step 7](#7-keep-your-permission-checks) | | `fetchReply()`, `reply({ withResponse: true })` | Not yet. Use the message `editReply()` returns | | `Options`, `ShardingManager` | Not exported. Remove them: Lamma needs no shards | | `discord.com/channels/…` links | Use `message.url` or Lamma's own links | | Libraries that look people up on Discord by id | Remove them. Lamma ids aren't Discord ids, so they would find the wrong person | ## 2. Make an app and get a token Follow [Build your first bot](https://lammapp.sa/developers/quick-start) to create the app, copy its token and install it in a test server. Ask for the scopes and intents your bot really uses. Discord ids saved in your database won't match anything on Lamma. Plan to set up each server's settings again. ## 3. Swap the package ```bash npm uninstall discord.js npm install lamma.js ``` ## 4. Change the import and the client ```js // Before import { Client, GatewayIntentBits, Partials } from 'discord.js'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], partials: [Partials.Message, Partials.Reaction] }); client.login(process.env.DISCORD_TOKEN); // After import { Client, GatewayIntentBits, Partials } from 'lamma.js'; const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent], partials: [Partials.Message, Partials.Reaction] }); client.login(process.env.LAMMA_TOKEN); ``` `login()` with no token reads `LAMMA_TOKEN`. To reach another copy of Lamma, such as a local one, set `LAMMA_URL` or `baseURL`. `partials` can stay: lamma.js always delivers events about uncached messages and users as partials. ## 5. Keep your intents Keep `GatewayIntentBits` as it is. Bits, arrays, flag names such as `'GuildMessages'` and `IntentsBitField` all work. | discord.js intent | On Lamma | | --- | --- | | `Guilds`, `GuildMessages`, `GuildMessageReactions` | Sent | | `MessageContent`, `GuildMembers` | Sent. Privileged | | `GuildModeration` (or `GuildBans`) | Sent. Also needs View audit log | | `GuildExpressions` (or `GuildEmojisAndStickers`) | Sent, for emoji | | `GuildPresences`, `GuildMessageTyping`, `DirectMessages`, `GuildVoiceStates`, `GuildInvites`, `GuildWebhooks`, `GuildScheduledEvents`, `AutoModeration*` | Ignored with one warning, and read as off | Privileged intents work in up to five servers you own while you develop, and everywhere once the app is [reviewed](https://lammapp.sa/developers/apps#privileged-access). In the app's settings, intents are listed by what they send, such as **New, edited and deleted messages** for `GuildMessages`. ## 6. Keep your events and handlers These work as they are: `Events.ClientReady` (`'clientReady'`, as in current discord.js), `MessageCreate`, `MessageUpdate`, `MessageDelete`, `MessageBulkDelete`, `MessageReactionAdd`, `MessageReactionRemove`, `InteractionCreate`, `GuildCreate`, `GuildDelete`, `GuildUpdate`, `GuildMemberAdd`, `GuildMemberUpdate`, `GuildMemberRemove`, `GuildBanAdd`, `GuildBanRemove`, `GuildAuditLogEntryCreate`, `ChannelCreate`, `ChannelUpdate`, `ChannelDelete`, `GuildRoleCreate`, `GuildRoleUpdate`, `GuildRoleDelete`, `GuildEmojiCreate`, `GuildEmojiUpdate`, `GuildEmojiDelete`, `Error`, `Warn` and `Raw`. Reaction events give `(reaction, user, { type, burst })`, as in discord.js. A reaction on a message the bot hasn't cached comes as a partial: check `reaction.partial` and call `reaction.fetch()`. The message cache keeps 200 messages per channel in memory, so `messageUpdate` and `messageDelete` carry the old message as they do in discord.js. Follow the [message cache rule](https://lammapp.sa/developers/developer-rules#message-text-stays-in-its-server). When the connection drops, the client reconnects by itself. There is no resume, so `ClientReady` fires again; fetch anything you missed with `channel.messages.fetch({ after })`. ## 7. Keep your permission checks `PermissionsBitField` and `PermissionFlagsBits` work as in discord.js: ```js if (!interaction.memberPermissions.has(PermissionFlagsBits.ManageMessages)) return; if (!interaction.appPermissions.has(PermissionFlagsBits.EmbedLinks)) return interaction.reply('I need Embed links here.'); const canKick = message.member.permissions.has(PermissionFlagsBits.KickMembers); ``` - `interaction.memberPermissions` and `interaction.appPermissions` are exact for the command's channel, including channel overrides. - `member.permissions` and `role.permissions` are server-wide, as in discord.js. - `guild.members.me`, `channel.permissionsFor(client.user)` and `me.permissionsIn(channel)` give the bot's own permissions. - Lamma doesn't send other members' channel overrides, so `channel.permissionsFor()` for anyone but the bot returns `null` and warns once. In a command, use `interaction.memberPermissions` instead. Lamma's permissions have Discord's flags. A few have different names in Lamma's settings: **Manage server** is `ManageGuild`, **Create invite** is `CreateInstantInvite`, **Create polls** is `SendPolls` and **Manage expressions** is `ManageGuildExpressions`. Lamma's **Mention roles** has no Discord flag, so it's left out. Discord flags for features Lamma doesn't have, such as `ManageThreads`, read as off unless the member is an administrator. ## 8. Update slash commands `SlashCommandBuilder` works as it is for string, integer, number, boolean, user, channel and role options, and `toJSON()` gives the same JSON as @discordjs/builders. A bot laid out like the discord.js guide keeps its layout: one `{ data, execute }` file per command in `commands/`, one file per event in `events/`, a `client.commands` `Collection`, and a `deploy-commands.js` that registers them with `REST` and `Routes`. ```js // commands/utility/ping.js import { SlashCommandBuilder } from 'lamma.js'; export default { data: new SlashCommandBuilder().setName('ping').setDescription('Check the bot'), async execute(interaction) { await interaction.reply('Pong!'); } }; // deploy-commands.js import { REST, Routes } from 'lamma.js'; const rest = new REST().setToken(process.env.LAMMA_TOKEN); await rest.put(Routes.applicationGuildCommands(clientId, guildId), { body: commands.map(command => command.data.toJSON()) }); ``` You can also register them from the client: ```js client.once(Events.ClientReady, async ready => { await ready.application.commands.set(commands); // every server, at once // await ready.application.commands.set(commands, guildId); // one server }); ``` - Global commands appear at once, so you don't need test-server tricks. - `setDefaultMemberPermissions()` isn't enforced yet and warns once. Keep an `interaction.memberPermissions` check in each sensitive command ([step 7](#7-keep-your-permission-checks)). - `setContexts()` and `setIntegrationTypes()` are accepted. Commands are used in servers only. - Mentionable and attachment options, autocomplete and subcommands throw when you add them, so you notice. - Split subcommands into separate commands, for example `/config-show` and `/config-set` instead of `/config show` and `/config set`. `interaction.reply()`, `deferReply()`, `editReply()`, `followUp()`, `flags: MessageFlags.Ephemeral` and the `interaction.options` getters work as before, within 3 seconds and 15 minutes. `reply({ withResponse: true })` gives `undefined` with one warning and `fetchReply()` rejects, so use the message `editReply()` returns. ## 9. Small changes in messages and channels - `channel.type` is a `ChannelType`: `GuildText`, `GuildVoice` or `GuildCategory`. Voice channels have a text chat, as on Discord, so `isTextBased()` is true for them. - Ids are strings in the same format, but they are Lamma ids. - Embeds keep their fields and limits. Images, thumbnails and video are dropped for now. - Lamma doesn't notify on replies, so `allowedMentions.repliedUser` changes nothing. Everything else in `allowedMentions` works. - An ephemeral reply is never stored, so `editReply()` on one returns a plain object, not a `Message`. - On a rate limit, `retry_after` is in milliseconds. lamma.js waits and retries for you. ## 10. Test it 1. Run the bot against a test server you own. 2. Watch for `warn` events: lamma.js warns once for each intent, call or option Lamma doesn't support, so nothing fails silently. 3. Try every command and the events your bot relies on. 4. When it works, share the install link with the servers that use it. Anything missing that your bot needs? Email [lamma.help@gmail.com](mailto:lamma.help@gmail.com) and say what it is. --- # Migrate from discord.py > Move a discord.py 2.x bot to lamma.py step by step, with every change your code needs. This guide moves a discord.py 2.x bot to [lamma.py](https://lammapp.sa/developers/lamma-py) one step at a time. Most bots keep their cogs, prefix commands, app command trees and event handlers. What changes is the import, the address, the token, a few names, and any feature Lamma doesn't offer bots. > [!NOTE] > lamma.py isn't published to PyPI yet. You can read this guide and check your bot against it now, then switch once the package is released. ## The short version For most bots, the port is two import lines and two environment variables: ```diff - import discord - from discord.ext import commands - from discord import app_commands + import lamma as discord + from lamma.ext import commands + from lamma import app_commands ``` ```bash export LAMMA_TOKEN=lmb1_... ``` Lamma's address is built in, as Discord's is. `bot.run()` reads `LAMMA_TOKEN` when you pass no token. If you keep the Lamma token in `DISCORD_TOKEN`, `bot.run(os.environ['DISCORD_TOKEN'])` works too. Python can't load `from discord import ...` from a package imported under another name, so that line becomes `from lamma import ...`. `discord.app_commands` and `discord.ext.commands` work as they are. Everything else stays as it is: intents, cogs and extensions, the command tree, checks, `discord.utils`, `discord.Embed` and the rest. The steps below cover the features Lamma handles differently. ## 1. Check what your bot uses Search your code for the features below first. Each needs a change, or is switched off on Lamma. | If your bot uses | On Lamma | | --- | --- | | `user.send()`, `create_dm()`, DM commands | Bots can't DM people. The call raises `Forbidden` with code `50007`, like a person with DMs closed | | `intents.presences`, `member.status`, `member.activity`, `on_presence_update` | Never sent to bots. `intents.presences` reads as `False`, so code that checks it switches itself off | | `channel.typing()`, `on_typing` | Does nothing, with one warning | | `change_presence()`, `activity=` and `status=` options | Does nothing for now, with one warning | | `discord.ui.View`, `Button`, `Select`, `Modal` | Not yet. Rewrite those flows as app command parameters | | `app_commands.Group`, `commands.GroupCog`, autocomplete, context menus, `Attachment` parameters | Not yet. `Group` and `GroupCog` raise so you notice. Use separate commands | | `discord.File`, `file=` and `files=`, embed images and thumbnails | Not yet. Images are dropped, and files aren't sent, with one warning | | `member.ban()`, `kick()`, `timeout()`, `add_roles()`, deleting other people's messages | Not in v1. Bots can still hear about bans, kicks and timeouts | | Voice (`VoiceClient`, `connect()`) | Not offered to bots | | Threads, forums, stages, stickers | Lamma doesn't have them yet | | `channel.permissions_for(member)` for someone other than the bot | Raises `ClientException`. See [step 7](#7-check-permissions) | | `@commands.has_permissions()` | Checks the author's server-wide permissions, without channel overrides, with one warning. See [step 7](#7-check-permissions) | | `AutoShardedBot`, `shard_id` | Not needed: one connection per bot. Use `commands.Bot` | | `discord.com/channels/…` links | Use `message.jump_url` or Lamma's own links | | Libraries that look people up on Discord by id | Remove them. Lamma ids aren't Discord ids, so they would find the wrong person | ## 2. Make an app and get a token Follow [Build your first bot](https://lammapp.sa/developers/quick-start) to create the app, copy its token and install it in a test server. Ask for the scopes and intents your bot really uses. Discord ids saved in your database won't match anything on Lamma. Plan to set up each server's settings again. ## 3. Swap the package ```bash pip uninstall discord.py pip install lamma.py ``` lamma.py needs Python 3.10 or newer and `aiohttp`. ## 4. Change the import and the bot The quickest way is to import lamma.py under the name your code already uses: ```python # Before import discord from discord.ext import commands from discord import app_commands bot = commands.Bot(command_prefix='!', intents=discord.Intents.default()) bot.run(os.environ['DISCORD_TOKEN']) # After import lamma as discord from lamma.ext import commands from lamma import app_commands bot = commands.Bot(command_prefix='!', intents=discord.Intents.default()) bot.run(os.environ['LAMMA_TOKEN']) ``` `run()` with no token reads `LAMMA_TOKEN`. To reach another copy of Lamma, such as a local one, set `LAMMA_URL` or `base_url`. ## 5. Check your intents | discord.py | In your app's settings | Notes | | --- | --- | --- | | `guilds` | Server, channel and role changes | | | `guild_messages` (`messages`) | New, edited and deleted messages | | | `message_content` | The text of every message it can see | Privileged | | `guild_reactions` (`reactions`) | Reactions | | | `members` | Members joining, leaving and changing | Privileged | | `moderation` (`bans`) | Bans, timeouts and audit log entries | Also needs View audit log | | `expressions` (`emojis`) | Custom emoji changes | Emoji only | | `presences`, `typing`, `dm_messages`, `voice_states`, `invites`, `webhooks`, `guild_scheduled_events`, `auto_moderation` | none | Ignored with one warning, and read as `False` | Keep using `discord.Intents` as you do now. lamma.py sends Lamma what it needs. Privileged intents work in up to five servers you own while you develop, and everywhere once the app is [reviewed](https://lammapp.sa/developers/apps#privileged-access). ## 6. Keep your events, cogs and extensions A bot laid out the usual discord.py way runs as it is: a `bot.py` whose `setup_hook` calls `await self.load_extension('cogs.fun')` for each file in a `cogs/` folder, and an `async def setup(bot)` in each file that adds a `commands.Cog`. `unload_extension()`, `reload_extension()` and `bot.extensions` work too. lamma.py's examples use this layout: ```text bot.py starts the bot and loads every file in cogs/ in setup_hook cogs/general.py prefix commands cogs/slash.py slash commands, one with a permission check cogs/events.py an event listener ``` These work as they are too: `setup_hook`, `on_ready`, `on_message`, `on_message_edit`, `on_message_delete`, the `on_raw_*` message events, `on_guild_join`, `on_guild_remove`, `on_member_join`, `on_member_update`, `on_member_remove`, `on_member_ban`, `on_member_unban`, `on_audit_log_entry_create`, the `on_guild_role_*` and `on_guild_channel_*` events, `on_guild_emojis_update`, `on_interaction` and `on_error`. So do `@bot.command()`, `Context`, converters, checks, `commands.Cog` with `cog_check`, `cog_command_error` and `cog_app_command_error`, `Cog.listener()`, `bot.add_cog()`, `bot.wait_for()`, `discord.utils.get`, `utils.format_dt` and `utils.escape_markdown`. Two changes: - Reaction events are raw only. Move `on_reaction_add` and `on_reaction_remove` code to `on_raw_reaction_add` and `on_raw_reaction_remove`. - `client.fetch_user()` only returns people the bot has already seen in its servers. The message cache keeps up to 1,000 messages in memory, so `on_message_edit` and `on_message_delete` get the old message as in discord.py. Follow the [message cache rule](https://lammapp.sa/developers/developer-rules#message-text-stays-in-its-server). When the connection drops, the client reconnects by itself and `on_ready` runs again. There is no resume, so fetch anything you missed with `channel.history(after=...)`. ## 7. Check permissions `discord.Permissions` works as before, with the same attribute names. `role.permissions`, `member.guild_permissions`, `interaction.permissions` and `interaction.app_permissions` are all `Permissions`: ```python if not interaction.permissions.manage_messages: await interaction.response.send_message('You need Manage messages.', ephemeral=True) return ``` - `interaction.permissions` is what the person who ran the command may do in that channel, with roles and channel overrides. - `interaction.app_permissions` is what the bot may do there. - `member.guild_permissions` is server-wide, from roles only, as in discord.py. The owner and administrators have every permission. - `channel.permissions_for(guild.me)` gives the bot's own permissions in a channel. Lamma doesn't send channel overrides to bots, so `permissions_for()` raises `ClientException` for anyone else rather than guess. Use `interaction.permissions` or `member.guild_permissions` instead. - Lamma's Mention roles permission has no Discord flag, so it isn't part of `Permissions`. The permission checks work as they are: | Check | Uses | | --- | --- | | `@app_commands.checks.has_permissions()` | `interaction.permissions`, exact for the channel. Raises `app_commands.MissingPermissions` | | `@app_commands.checks.bot_has_permissions()` | `interaction.app_permissions`, exact for the channel. Raises `app_commands.BotMissingPermissions` | | `@commands.bot_has_permissions()` | `channel.permissions_for(guild.me)`, exact for the channel. Raises `commands.BotMissingPermissions` | | `@commands.has_permissions()` | The author's server-wide permissions, because Lamma doesn't send channel overrides. It warns once. Raises `commands.MissingPermissions` | | `@commands.has_guild_permissions()`, `@commands.bot_has_guild_permissions()` | `guild_permissions`, as in discord.py | `@app_commands.default_permissions()` is accepted but not enforced yet, so add `@app_commands.checks.has_permissions()` to each sensitive command. discord.py's flag classes are there with the same names and bits: `Permissions` with its presets such as `Permissions.text()`, `MessageFlags`, `PublicUserFlags`, `MemberFlags`, `ChannelFlags`, `RoleFlags`, `AttachmentFlags`, `EmbedFlags`, `SystemChannelFlags`, `ApplicationFlags`, `MemberCacheFlags` and the rest. Lamma has none of the features most of them describe, so `message.flags`, `user.public_flags`, `member.flags`, `channel.flags` and `role.flags` are always empty. ## 8. Update app commands `app_commands.CommandTree`, `@tree.command`, `describe`, `choices`, `rename`, `Choice`, `Range` and `Literal` work as they are, with `str`, `int`, `float`, `bool`, `Member`, `User`, `TextChannel`, `VoiceChannel` and `Role` parameters. ```python class MyBot(commands.Bot): async def setup_hook(self): await self.tree.sync() # every server, at once # await self.tree.sync(guild=lamma.Object(id)) # one server ``` - Global commands appear at once, so you don't need `copy_global_to` tricks while testing. - `default_permissions` and `guild_only` are accepted but not enforced yet. Keep a permission check in each sensitive command ([step 7](#7-check-permissions)). - Channel options aren't limited by type yet. If someone picks a voice channel for a `TextChannel` parameter, the command fails with `app_commands.TransformerError`. - Split groups into separate commands, for example `/config-show` and `/config-set` instead of `/config show` and `/config set`. `app_commands.Group` and `commands.GroupCog` raise `NotImplementedError` so a group is never registered half working. `interaction.response.send_message(ephemeral=True)`, `defer()`, `edit_original_response()` and `followup.send()` work as before, within 3 seconds and 15 minutes. ## 9. Small changes in messages and channels - Ids are ints, as in discord.py. They are Lamma ids, so ids saved from Discord won't match. - Text channels are `TextChannel` and voice channels are `VoiceChannel`, each with `channel.type`. Lamma voice channels have a text chat, so bots can send messages there. Categories are `CategoryChannel`, with `category.text_channels` and `category.voice_channels`; `channel.category` and `guild.categories` work too. There are no threads, forums or stages. - Embeds keep their fields and limits. Images, thumbnails and video are dropped for now. - Lamma doesn't notify on replies, so `AllowedMentions(replied_user=...)` changes nothing. Everything else in `AllowedMentions` works. - An ephemeral reply is never stored, so editing or following up on one returns a plain dict, not a `Message`. - On a rate limit, `retry_after` is in milliseconds. lamma.py waits and retries for you. ## 10. Test it 1. Run the bot against a test server you own. 2. Watch the log: lamma.py warns once for each intent, call or option Lamma doesn't support, so nothing fails silently. 3. Try every command and the events your bot relies on. 4. When it works, share the install link with the servers that use it. Anything missing that your bot needs? Email [lamma.help@gmail.com](mailto:lamma.help@gmail.com) and say what it is. --- # Developer rules > The rules every Lamma app follows to keep people's trust. People trust Lamma with their conversations. These rules keep that trust when they add your app. Breaking them can get an app switched off. ## Ask only for what you use Ask for the scopes, intents and permissions your app needs today, and nothing more. The install screen shows people every one of them. If a feature needs `message_content` or `guild_members`, say why when you ask for review. ## Be open about your data Your app runs on infrastructure you choose, outside Lamma. Give your app a privacy policy link that says what it keeps, why, for how long and where. People see it on the install screen. ## Message text stays in its server The SDKs keep recent messages in memory so edit and delete events can show what changed, and so a mod-log bot can post a deleted message in that server's log channel. That cache has rules: - Keep it in memory and bounded. lamma.js keeps 200 messages per channel and lamma.py 1,000 in total, oldest dropped first. Neither writes it to disk. - Drop a channel's messages when the channel is deleted or hidden from the bot, and a server's when the bot leaves it. - Use cached text only for a feature in the same server, such as its own mod log. - Never copy message text to your own storage, logs, analytics or other services, and never keep it longer than the cache does. ## Respect what Lamma keeps private Don't try to work out who is online, who is typing or what people say in direct messages, and don't collect people's details across servers. Lamma doesn't give these to apps on purpose. ## Keep tokens and webhook URLs secret Store them like passwords. If one leaks, revoke it at once in **User Settings › Developer**, or make a new webhook URL. ## No spam, no impersonation Don't post unasked-for messages, mass-mention people, or make an app look like Lamma, Lamma staff or another person. Bots carry an **App** badge for that reason. ## Reporting security issues Found a security problem in Lamma or its API? Email [lamma.help@gmail.com](mailto:lamma.help@gmail.com) with the details, and give us time to fix it before telling anyone else.