# 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.
