ILLEST Developers

Where it differs from discord.py

illest.py keeps discord.py's names, arguments and behaviour wherever ILLEST allows it. Each difference below is deliberate. Most follow from one fact: ILLEST is end-to-end encrypted. The server stores ciphertext and holds no keys, so it cannot read anything for a bot.

Signing in and joining

A bot is a device.

Its token signs it in. The first run enrols a device with its own key, kept under ~/.illest/bots/<name>/. Keep that folder with the bot: losing it is losing the device, and history only that device held. A second machine running the same token is a second device.

Joining a server is two steps, not one.

The invite link adds the bot to the server. The clients of people in the server then let its device into each channel's encrypted group, usually within seconds while it is online. Until a channel has let it in, the bot cannot read or send there. on_guild_join fires on the first step; a channel's messages arrive after the second.

A bot that was stopped catches up by itself.

When it starts, it brings every group it holds up to date before handling anything. What was sent while it was away arrives in on_missed_message (the message's late is True), not on_message. Turn that off with Client(read_missed=False).

Reading

History starts when the bot joined.

channel.history(), fetch_message() and channel.pins() return the messages this bot holds, from when it was let into the channel's group. There is nothing older to fetch, because the server cannot read it either.

Who sent it is what their device proves.

message.author_verified, interaction.user_verified and ctx.author_verified are True when the sender's own device vouched for the account. When False, author is only the server's word: authorise nothing on it. The library remembers each account's key. If it changes, author_key_changed is True and on_key_change(user_id) fires once. Call client.accept_key_change(user_id) once you have checked with the person another way.

Commands and interactions

Commands, presses, picks and forms travel sealed.

Using a slash command, pressing a button, picking from a select menu or sending a form seals a message into the conversation that only the bot acts on. The server never learns which command was used or with what. So there is no interaction token, no webhook URL, and no 3-second deadline. defer() still shows "thinking" until the answer replaces it. Answers and follow-ups are taken for 15 minutes, as discord takes them.

ephemeral=True is a rendering rule.

Every ILLEST app shows an ephemeral message to its one reader only. The bytes still reach every device in the conversation, because inside an encrypted group nothing stronger exists. A form (send_modal) and autocomplete suggestions are delivered the same way. Put nothing in one that the other members must not be able to read.

Pictures travel with the message.

An address in an embed (set_image, set_thumbnail, an author or footer icon) or in a layout (Thumbnail, MediaGallery) is fetched once, by the bot's own server, when the message is sent. The picture is sealed with the message as a file named picture-1.png and so on, and the address becomes attachment://picture-1.png. Nobody's app ever fetches an outside address, so a picture cannot tell its host who read the message, or when. A server that turned this off leaves the address as it is, and apps show it as a link to open. attachment://name names a file sent with the same message, as before.

A layout needs no flag.

A LayoutView is sent as view=, beside content and embeds if you like, and an app that has never seen a layout shows the text and buttons it holds. Up to 40 parts, 4000 characters of text in all, one container deep.

An answer can be a short notice.

interaction.response.send_toast("Saved") shows a few words to the person who pressed, for a moment, and alert=True keeps them up until dismissed. Nothing is added to the conversation they see.

A message can ask for a reply.

channel.send("What should the poll ask?", ask_reply=True) opens the reply box on that message, for everyone or for the one person AskReply(user=...) names. What they write is an ordinary reply, read like any other message.

Forms hold more than text.

Beside TextInput, a form takes NumberInput, any select menu, RadioGroup, CheckboxGroup, Checkbox, FileUpload and TextDisplay, each titled with ui.Label. The app checks every answer before it goes, and the library checks again; when an answer breaks a field's rules, the person is told privately and on_submit does not run.

tree.sync() takes effect at once.

Global commands appear immediately; nothing propagates over an hour. A bot built on commands.Bot with no slash commands lists its prefix commands in the app's / menu by itself on each start (sync_commands=False turns that off). A bot with slash commands lists both when it calls tree.sync().

default_permissions is checked twice.

The app hides a command from members who lack the permissions. The library refuses it again with MissingPermissions, since the app's menu is only a convenience.

Prefix commands also answer to /.

While the / menu is synced, picking a prefix command there sends /name ... as typed words, so / is read as a prefix too.

Permissions, intents and scopes

Permissions have discord's names and ILLEST's bits.

illest.Permissions(ban_members=True) works as in discord.py, but .value is ILLEST's number. A number from a discord permissions calculator means something else here, so build invite links with Permissions or the portal's Installation page. Where discord has a name ILLEST does not, the name maps to the bit that covers it: read_message_history is view_channel, and embed_links is send_messages. Permissions.ILLEST_ONLY lists ILLEST's own bits, such as share_screen and manage_stage.

Intents filter; scopes grant.

The server pushes what the bot is a member of, and an intent decides which of those events reach your handlers. A message whose intent is off is not even decrypted. What the bot may read and do is set by its scopes, chosen by its owner in the portal:

ScopeWhat it allows
messages.readreading messages
messages.writesending messages
guilds.readreading servers
members.readreading member lists
moderatemoderation actions
guilds.managemaking and changing channels, roles and invites

Intents.default() includes members and message_content, which are scopes here rather than privileged intents.

The server's roles still apply.

A bot with the moderate scope can ban only where its role holds ban_members, above the member it bans, as on discord.

Ids and time

Ids.

User ids are integers, as on discord. Message, channel, server and role ids are 20-digit snowflakes, which the library hands you as int. created_at is read from the id as discord.py reads it.

What a bot cannot do

discord.pyillest.pyWhy
VoiceChannel.connect(), VoiceClientraises ClientExceptionCalls are encrypted from each person's device, and a bot has no microphone.
Guild.edit(), Guild.delete()raisesA server's own settings belong to its managers, in the app.
ClientUser.edit(username=...)raisesA bot's name is changed by its owner, in the portal.
Message.remove_reaction (someone else's), clear_reactionsraisesReactions are sealed in the conversation; only their author can take one back.
TextChannel.follow(), channel webhooksraises / emptyILLEST has no channel following. Outbound webhooks are a server setting.
Sharding (AutoShardedClient, shard_id)AutoShardedBot is BotOne connection serves any number of servers.
Scheduled events, stickers, polls through the APInot presentILLEST's polls are sealed messages; the others are not yet there for bots.

Rate limits and reconnects

The library paces each action to the server's own limits before sending, so a busy bot waits rather than being refused. It reconnects on its own with backoff, and run() sets up logging as discord.py does.