illest 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- illest/__init__.py +169 -0
- illest/_limits.py +240 -0
- illest/_version.py +3 -0
- illest/abc.py +257 -0
- illest/activity.py +100 -0
- illest/app_commands/__init__.py +76 -0
- illest/app_commands/checks.py +161 -0
- illest/app_commands/commands.py +807 -0
- illest/app_commands/errors.py +130 -0
- illest/app_commands/namespace.py +31 -0
- illest/app_commands/tree.py +341 -0
- illest/attachment.py +161 -0
- illest/auth.py +286 -0
- illest/buttons.py +160 -0
- illest/client.py +2272 -0
- illest/colour.py +156 -0
- illest/content.py +470 -0
- illest/embeds.py +362 -0
- illest/enums.py +249 -0
- illest/envelope.py +97 -0
- illest/errors.py +165 -0
- illest/ext/__init__.py +1 -0
- illest/ext/commands/__init__.py +127 -0
- illest/ext/commands/bot.py +538 -0
- illest/ext/commands/cog.py +174 -0
- illest/ext/commands/converter.py +412 -0
- illest/ext/commands/core.py +1164 -0
- illest/ext/commands/errors.py +343 -0
- illest/ext/commands/flags.py +156 -0
- illest/ext/commands/help.py +382 -0
- illest/ext/commands/hybrid.py +153 -0
- illest/ext/tasks/__init__.py +391 -0
- illest/file.py +59 -0
- illest/gateway.py +513 -0
- illest/intents.py +190 -0
- illest/interactions.py +564 -0
- illest/media.py +182 -0
- illest/mentions.py +41 -0
- illest/mls.py +843 -0
- illest/models.py +2562 -0
- illest/object.py +70 -0
- illest/permissions.py +317 -0
- illest/pins.py +152 -0
- illest/py.typed +0 -0
- illest/ratelimit.py +113 -0
- illest/replies.py +34 -0
- illest/rpc/__init__.py +45 -0
- illest/rpc/_async.py +355 -0
- illest/rpc/_sync.py +164 -0
- illest/rpc/_wire.py +223 -0
- illest/rpc/errors.py +66 -0
- illest/ui/__init__.py +52 -0
- illest/ui/button.py +126 -0
- illest/ui/components.py +54 -0
- illest/ui/item.py +69 -0
- illest/ui/layout.py +417 -0
- illest/ui/modal.py +636 -0
- illest/ui/select.py +290 -0
- illest/ui/view.py +249 -0
- illest/utils.py +217 -0
- illest-2.2.0.dist-info/METADATA +151 -0
- illest-2.2.0.dist-info/RECORD +65 -0
- illest-2.2.0.dist-info/WHEEL +5 -0
- illest-2.2.0.dist-info/licenses/LICENSE +24 -0
- illest-2.2.0.dist-info/top_level.txt +1 -0
illest/__init__.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""illest: the Python library for ILLEST, for bots and Rich Presence.
|
|
2
|
+
|
|
3
|
+
from illest.ext import commands
|
|
4
|
+
|
|
5
|
+
bot = commands.Bot(command_prefix="/", token="illb_...")
|
|
6
|
+
|
|
7
|
+
@bot.command(name="ping")
|
|
8
|
+
async def ping(ctx):
|
|
9
|
+
await ctx.reply("pong")
|
|
10
|
+
|
|
11
|
+
bot.run()
|
|
12
|
+
|
|
13
|
+
## The everyday names
|
|
14
|
+
|
|
15
|
+
`Client`, `Intents`, `Embed`, `ButtonStyle`, `ui.Button`, `ext.commands.Bot`,
|
|
16
|
+
`Context`, `@bot.event`, `@bot.command()`, `ctx.send/reply`,
|
|
17
|
+
`message.reply`, `embed=`, `view=`, `ephemeral=True`. Moving a bot written
|
|
18
|
+
for another library: the migration guide on the developer site.
|
|
19
|
+
|
|
20
|
+
## What is different, and why
|
|
21
|
+
|
|
22
|
+
This network is end-to-end encrypted. The server stores ciphertext and holds
|
|
23
|
+
no keys, so:
|
|
24
|
+
|
|
25
|
+
* **A bot is a real device.** Its token ENROLs it; its device key (generated
|
|
26
|
+
once, kept under `~/.illest/bots/`) is what proves it at sign-in. Losing
|
|
27
|
+
that file is losing the device, which is losing history only it held.
|
|
28
|
+
* **A bot joins groups like a member.** Invite it to your server; somebody's
|
|
29
|
+
client lets it into each channel's group. "Invited" and "able to read" are
|
|
30
|
+
different moments: usually seconds apart while it is online.
|
|
31
|
+
* **A bot that was stopped catches up by itself.** While it is away its groups move on (members join and leave, keys are
|
|
32
|
+
replaced). When it starts, and when it signs in again after the server no longer held its session, it brings every group it
|
|
33
|
+
holds up to the server's epoch, a channel at a time and unhurried, before it handles anything; a channel it cannot catch up is
|
|
34
|
+
logged by its id and the rest go on. A message that arrives sealed at an epoch ahead of the bot's makes it catch that channel up
|
|
35
|
+
once and try the message again; if that fails too the message is dropped, and logged without its content. Messages sent while the
|
|
36
|
+
bot was away are not fetched: it reads what arrives while it is connected.
|
|
37
|
+
* **Who sent it is what their device proves, not what the server says.** Every message and every button press carries
|
|
38
|
+
`author_id` (`sender_id` on an `Interaction`) from the server, and the library checks it against the sender's leaf in the group:
|
|
39
|
+
a frame that names an account other than the one the leaf proves is dropped. A sender whose leaf proves no account (a bot, or
|
|
40
|
+
a device with no certificate) is still delivered, but `Message.author_verified` (`Interaction.sender_verified`,
|
|
41
|
+
`Context.author_verified`) is False, and `author_id` is then only the server's word: do not authorise anything by it.
|
|
42
|
+
* **An account is the key it first showed.** A certificate proves that some account key vouched for a device, and anybody can
|
|
43
|
+
make a key. So the library remembers the first key each account shows (in `pins.json`, in its state folder) and holds the
|
|
44
|
+
account to it: an invitation written by a device under another key is turned down, a member the server lists who lets
|
|
45
|
+
themselves in under another key is not taken in, and a message from a device under another key is delivered with
|
|
46
|
+
`author_verified` False and `author_key_changed` True. The change is said once in the log and to `on_key_change(user_id)`.
|
|
47
|
+
It happens when a person sets their account up again, and when somebody else writes as them: ask them another way, then call
|
|
48
|
+
`client.accept_key_change(user_id)`. An invitation is opened only from a device the channel lists.
|
|
49
|
+
* **A button answers only to who could see it.** `@bot.on_button` runs only for a press on a message this bot sent, naming a
|
|
50
|
+
button that message carried and left enabled, from somebody who could see it (and, for a message meant for one reader, who is
|
|
51
|
+
proven to be that reader). The library remembers its buttons in its state folder, so they survive a restart.
|
|
52
|
+
* **Ephemeral is a rendering rule.** `ephemeral=True` marks a message
|
|
53
|
+
visible only to its one reader on every client that honours the rule. It
|
|
54
|
+
is not cryptography against other members of the group (no such thing
|
|
55
|
+
exists inside an MLS group), and this library says so rather than implying
|
|
56
|
+
otherwise.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from ._version import __version__
|
|
60
|
+
from . import abc, app_commands, ui, utils
|
|
61
|
+
from .activity import Activity, BaseActivity, CustomActivity, Game, Streaming
|
|
62
|
+
from .attachment import AttachmentError
|
|
63
|
+
from .client import Client
|
|
64
|
+
from .colour import Color, Colour
|
|
65
|
+
from .content import Button, ContentError
|
|
66
|
+
from .embeds import Embed
|
|
67
|
+
from .enums import * # noqa: F401,F403
|
|
68
|
+
from .enums import __all__ as _enums
|
|
69
|
+
from .errors import (
|
|
70
|
+
AuthFailed,
|
|
71
|
+
BadRequest,
|
|
72
|
+
BotForbidden,
|
|
73
|
+
ClientException,
|
|
74
|
+
ConnectionClosed,
|
|
75
|
+
DiscordException,
|
|
76
|
+
DiscordServerError,
|
|
77
|
+
Forbidden,
|
|
78
|
+
GatewayError,
|
|
79
|
+
GatewayNotFound,
|
|
80
|
+
HTTPException,
|
|
81
|
+
IllestException,
|
|
82
|
+
InteractionResponded,
|
|
83
|
+
InvalidData,
|
|
84
|
+
LoginFailure,
|
|
85
|
+
MissingScope,
|
|
86
|
+
NotFound,
|
|
87
|
+
PrivilegedIntentsRequired,
|
|
88
|
+
RateLimited,
|
|
89
|
+
ServerError,
|
|
90
|
+
StaleEpoch,
|
|
91
|
+
UnknownAction,
|
|
92
|
+
)
|
|
93
|
+
from .file import File
|
|
94
|
+
from .replies import AskReply
|
|
95
|
+
from .ui.layout import MediaGalleryItem, SeparatorSpacing, UnfurledMediaItem
|
|
96
|
+
from .intents import Intents
|
|
97
|
+
from .interactions import Interaction, InteractionCallbackResponse, InteractionResponse, Webhook, WebhookMessage
|
|
98
|
+
from .mentions import AllowedMentions
|
|
99
|
+
from .models import * # noqa: F401,F403
|
|
100
|
+
from .models import __all__ as _models
|
|
101
|
+
from .object import Object
|
|
102
|
+
from .permissions import PermissionOverwrite, Permissions
|
|
103
|
+
from .ui.select import SelectOption
|
|
104
|
+
from .utils import setup_logging
|
|
105
|
+
|
|
106
|
+
#: The scopes a bot token can hold, as the server names them (`actions::bots::SCOPES`).
|
|
107
|
+
SCOPES = ("messages.read", "messages.write", "guilds.read", "members.read", "moderate", "guilds.manage")
|
|
108
|
+
|
|
109
|
+
__all__ = [
|
|
110
|
+
"SCOPES",
|
|
111
|
+
"AllowedMentions",
|
|
112
|
+
"Activity",
|
|
113
|
+
"AttachmentError",
|
|
114
|
+
"AuthFailed",
|
|
115
|
+
"BadRequest",
|
|
116
|
+
"BaseActivity",
|
|
117
|
+
"BotForbidden",
|
|
118
|
+
"Button",
|
|
119
|
+
"Client",
|
|
120
|
+
"ClientException",
|
|
121
|
+
"Color",
|
|
122
|
+
"Colour",
|
|
123
|
+
"ConnectionClosed",
|
|
124
|
+
"ContentError",
|
|
125
|
+
"CustomActivity",
|
|
126
|
+
"DiscordException",
|
|
127
|
+
"DiscordServerError",
|
|
128
|
+
"AskReply",
|
|
129
|
+
"Embed",
|
|
130
|
+
"MediaGalleryItem",
|
|
131
|
+
"SeparatorSpacing",
|
|
132
|
+
"UnfurledMediaItem",
|
|
133
|
+
"File",
|
|
134
|
+
"Forbidden",
|
|
135
|
+
"Game",
|
|
136
|
+
"GatewayError",
|
|
137
|
+
"GatewayNotFound",
|
|
138
|
+
"HTTPException",
|
|
139
|
+
"IllestException",
|
|
140
|
+
"Intents",
|
|
141
|
+
"Interaction",
|
|
142
|
+
"InteractionCallbackResponse",
|
|
143
|
+
"InteractionResponded",
|
|
144
|
+
"InteractionResponse",
|
|
145
|
+
"InvalidData",
|
|
146
|
+
"LoginFailure",
|
|
147
|
+
"MissingScope",
|
|
148
|
+
"NotFound",
|
|
149
|
+
"Object",
|
|
150
|
+
"PermissionOverwrite",
|
|
151
|
+
"Permissions",
|
|
152
|
+
"SelectOption",
|
|
153
|
+
"PrivilegedIntentsRequired",
|
|
154
|
+
"RateLimited",
|
|
155
|
+
"ServerError",
|
|
156
|
+
"StaleEpoch",
|
|
157
|
+
"Streaming",
|
|
158
|
+
"UnknownAction",
|
|
159
|
+
"Webhook",
|
|
160
|
+
"WebhookMessage",
|
|
161
|
+
"__version__",
|
|
162
|
+
"abc",
|
|
163
|
+
"app_commands",
|
|
164
|
+
"setup_logging",
|
|
165
|
+
"ui",
|
|
166
|
+
"utils",
|
|
167
|
+
*_enums,
|
|
168
|
+
*_models,
|
|
169
|
+
]
|
illest/_limits.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Every action's rate limit, as the server enforces it: generated by `scripts/gen_limits.py` from the server's action table.
|
|
2
|
+
|
|
3
|
+
Do not edit by hand. Each entry is `(rate, burst)`: a bucket of `burst` requests per account, refilled at `rate` a second. An
|
|
4
|
+
action not listed takes `DEFAULT`, which is also what the server gives an action registered without a limit of its own.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
DEFAULT: tuple[float, int] = (2.0, 10)
|
|
8
|
+
|
|
9
|
+
LIMITS: dict[str, tuple[float, int]] = {
|
|
10
|
+
"account.credential.move.begin": (0.5, 3),
|
|
11
|
+
"account.credential.move.complete": (0.5, 3),
|
|
12
|
+
"account.delete": (0.2, 3),
|
|
13
|
+
"account.delete.begin": (0.2, 3),
|
|
14
|
+
"account.delete.complete": (0.2, 3),
|
|
15
|
+
"account.identity.unlock.begin": (0.2, 3),
|
|
16
|
+
"account.identity.unlock.complete": (0.2, 3),
|
|
17
|
+
"account.opaque.upgrade.begin": (0.5, 3),
|
|
18
|
+
"account.opaque.upgrade.complete": (0.5, 3),
|
|
19
|
+
"account.password.change": (0.5, 3),
|
|
20
|
+
"account.password.change.begin": (0.5, 3),
|
|
21
|
+
"account.password.change.complete": (0.5, 3),
|
|
22
|
+
"account.recovery.begin": (0.5, 3),
|
|
23
|
+
"account.recovery.complete": (0.5, 3),
|
|
24
|
+
"account.retention.set": (10.0, 60),
|
|
25
|
+
"account.step_up": (1.0, 5),
|
|
26
|
+
"admin.ban": (2.0, 10),
|
|
27
|
+
"admin.config": (4.0, 10),
|
|
28
|
+
"admin.config_set": (4.0, 10),
|
|
29
|
+
"admin.disconnect": (2.0, 10),
|
|
30
|
+
"admin.log": (2.0, 10),
|
|
31
|
+
"admin.role_set": (2.0, 10),
|
|
32
|
+
"admin.roster": (2.0, 10),
|
|
33
|
+
"admin.unban": (2.0, 10),
|
|
34
|
+
"auth.credential": (1.0, 10),
|
|
35
|
+
"auth.device.enroll": (0.2, 3),
|
|
36
|
+
"auth.device.enroll.verify": (0.2, 3),
|
|
37
|
+
"auth.login.begin": (1.0, 5),
|
|
38
|
+
"auth.login.complete": (1.0, 5),
|
|
39
|
+
"auth.login.verify": (1.0, 5),
|
|
40
|
+
"auth.recover.begin": (1.0, 5),
|
|
41
|
+
"auth.recover.complete": (0.5, 3),
|
|
42
|
+
"auth.recover.verify": (0.2, 3),
|
|
43
|
+
"auth.register.begin": (0.2, 3),
|
|
44
|
+
"auth.register.complete": (1.0, 5),
|
|
45
|
+
"auth.register.work": (0.5, 5),
|
|
46
|
+
"auth.reset.begin": (1.0, 5),
|
|
47
|
+
"auth.reset.complete": (0.2, 3),
|
|
48
|
+
"badge.elite": (4.0, 60),
|
|
49
|
+
"badge.grant": (4.0, 60),
|
|
50
|
+
"badge.revoke": (4.0, 60),
|
|
51
|
+
"bot.commands.get": (2.0, 20),
|
|
52
|
+
"bot.commands.set": (0.5, 5),
|
|
53
|
+
"bot.create": (0.5, 5),
|
|
54
|
+
"bot.delete": (1.0, 10),
|
|
55
|
+
"bot.identify": (1.0, 10),
|
|
56
|
+
"bot.identity.publish": (1.0, 5),
|
|
57
|
+
"bot.token.create": (1.0, 10),
|
|
58
|
+
"bot.update": (1.0, 10),
|
|
59
|
+
"channel.devices": (8.0, 40),
|
|
60
|
+
"channel.disappear.set": (10.0, 60),
|
|
61
|
+
"channel.dm.open": (1.0, 10),
|
|
62
|
+
"channel.get": (8.0, 40),
|
|
63
|
+
"channel.group.add": (10.0, 60),
|
|
64
|
+
"channel.group.create": (4.0, 60),
|
|
65
|
+
"channel.group.leave": (10.0, 60),
|
|
66
|
+
"channel.group.update": (10.0, 60),
|
|
67
|
+
"channel.messages.list": (8.0, 40),
|
|
68
|
+
"channel.messages.purge": (10.0, 30),
|
|
69
|
+
"channel.typing": (6.0, 10),
|
|
70
|
+
"friend.request": (0.5, 10),
|
|
71
|
+
"guild.application.submit": (1.0, 5),
|
|
72
|
+
"guild.audit": (4.0, 10),
|
|
73
|
+
"guild.create": (1.0, 30),
|
|
74
|
+
"guild.insights": (1.0, 5),
|
|
75
|
+
"guild.invite.create": (2.0, 20),
|
|
76
|
+
"guild.invite.preview": (3.0, 20),
|
|
77
|
+
"guild.join": (2.0, 30),
|
|
78
|
+
"guild.members": (3.0, 15),
|
|
79
|
+
"guild.onboarding.answer": (3.0, 20),
|
|
80
|
+
"guild.onboarding.preview": (3.0, 20),
|
|
81
|
+
"guild.onboarding.seen": (3.0, 20),
|
|
82
|
+
"guild.order.set": (1.0, 10),
|
|
83
|
+
"guild.overrides": (4.0, 10),
|
|
84
|
+
"guild.profile.get": (3.0, 15),
|
|
85
|
+
"guild.template.apply": (0.2, 2),
|
|
86
|
+
"guild.template.export": (1.0, 5),
|
|
87
|
+
"guild.vanity.check": (4.0, 30),
|
|
88
|
+
"guild.vanity.resolve": (3.0, 30),
|
|
89
|
+
"identity.certify": (1.0, 5),
|
|
90
|
+
"identity.publish": (1.0, 5),
|
|
91
|
+
"link.preview": (0.5, 10),
|
|
92
|
+
"links.add": (0.2, 6),
|
|
93
|
+
"links.mine": (2.0, 10),
|
|
94
|
+
"links.order": (1.0, 10),
|
|
95
|
+
"links.remove": (1.0, 10),
|
|
96
|
+
"links.services": (1.0, 5),
|
|
97
|
+
"links.update": (1.0, 10),
|
|
98
|
+
"links.verify": (0.1, 4),
|
|
99
|
+
"links.view": (2.0, 12),
|
|
100
|
+
"market.attach": (0.1, 6),
|
|
101
|
+
"market.attachment": (2.0, 20),
|
|
102
|
+
"market.browse": (2.0, 20),
|
|
103
|
+
"market.mine": (1.0, 10),
|
|
104
|
+
"market.name": (2.0, 20),
|
|
105
|
+
"market.status": (2.0, 20),
|
|
106
|
+
"market.voucher.convert": (0.05, 6),
|
|
107
|
+
"market.voucher.receive": (0.1, 10),
|
|
108
|
+
"market.voucher.recovery": (0.01, 3),
|
|
109
|
+
"market.wallet.unlink": (0.1, 6),
|
|
110
|
+
"media.ticket": (1.0, 10),
|
|
111
|
+
"message.pin": (10.0, 30),
|
|
112
|
+
"message.redact": (10.0, 30),
|
|
113
|
+
"message.send": (5.0, 20),
|
|
114
|
+
"message.unpin": (10.0, 30),
|
|
115
|
+
"mls.commit.submit": (5.0, 20),
|
|
116
|
+
"mls.commits.since": (12.0, 60),
|
|
117
|
+
"mls.grant.clear": (1.0, 5),
|
|
118
|
+
"mls.grant.issue": (5.0, 20),
|
|
119
|
+
"mls.grant.needed": (12.0, 60),
|
|
120
|
+
"mls.group.reset": (0.2, 2),
|
|
121
|
+
"mls.group_info.publish": (5.0, 20),
|
|
122
|
+
"mls.key_packages.claim": (6.0, 30),
|
|
123
|
+
"mls.key_packages.publish": (1.0, 5),
|
|
124
|
+
"mls.key_packages.purge": (1.0, 5),
|
|
125
|
+
"name.available": (2.0, 20),
|
|
126
|
+
"name.claim": (0.05, 4),
|
|
127
|
+
"name.keep": (0.1, 6),
|
|
128
|
+
"name.primary": (0.1, 6),
|
|
129
|
+
"name.release": (0.1, 6),
|
|
130
|
+
"name.resolve": (2.0, 20),
|
|
131
|
+
"names.list": (2.0, 20),
|
|
132
|
+
"page.create": (2.0, 20),
|
|
133
|
+
"page.preview": (1.0, 3),
|
|
134
|
+
"page.preview_inputs": (1.0, 3),
|
|
135
|
+
"page.publish": (3.0, 30),
|
|
136
|
+
"page.restore": (2.0, 20),
|
|
137
|
+
"page.save": (10.0, 20),
|
|
138
|
+
"page.stats": (1.0, 5),
|
|
139
|
+
"pins.get": (2.0, 20),
|
|
140
|
+
"pins.put": (0.5, 10),
|
|
141
|
+
"platform.account.find": (1.0, 8),
|
|
142
|
+
"platform.account.flag": (1.0, 6),
|
|
143
|
+
"platform.account.premium": (1.0, 6),
|
|
144
|
+
"platform.get": (2.0, 10),
|
|
145
|
+
"platform.market": (1.0, 6),
|
|
146
|
+
"platform.market.export": (0.5, 4),
|
|
147
|
+
"platform.market.keygen": (0.2, 6),
|
|
148
|
+
"platform.market.prepare": (0.5, 4),
|
|
149
|
+
"platform.market.refuse": (1.0, 6),
|
|
150
|
+
"platform.market.seen": (1.0, 6),
|
|
151
|
+
"platform.security": (1.0, 5),
|
|
152
|
+
"platform.set": (2.0, 14),
|
|
153
|
+
"platform.shop": (1.0, 6),
|
|
154
|
+
"platform.shop.allow": (0.5, 6),
|
|
155
|
+
"platform.shop.close": (0.5, 6),
|
|
156
|
+
"platform.shop.forbid": (0.5, 6),
|
|
157
|
+
"platform.shop.reopen": (0.5, 6),
|
|
158
|
+
"platform.shop.takedown": (0.5, 6),
|
|
159
|
+
"platform.test": (0.2, 3),
|
|
160
|
+
"presence.set": (1.0, 5),
|
|
161
|
+
"report.list": (1.0, 10),
|
|
162
|
+
"report.settle": (1.0, 10),
|
|
163
|
+
"shop.listing.create": (0.2, 8),
|
|
164
|
+
"shop.listing.get": (3.0, 30),
|
|
165
|
+
"shop.listing.list": (3.0, 30),
|
|
166
|
+
"shop.listing.report": (0.2, 6),
|
|
167
|
+
"shop.listing.takedown": (0.5, 10),
|
|
168
|
+
"shop.listing.update": (0.5, 10),
|
|
169
|
+
"shop.listing.withdraw": (0.5, 10),
|
|
170
|
+
"shop.order.cancel": (0.5, 10),
|
|
171
|
+
"shop.order.confirm": (0.3, 8),
|
|
172
|
+
"shop.order.create": (0.2, 8),
|
|
173
|
+
"shop.order.deliver": (0.5, 10),
|
|
174
|
+
"shop.order.get": (3.0, 30),
|
|
175
|
+
"shop.order.list": (2.0, 20),
|
|
176
|
+
"shop.order.look": (0.3, 8),
|
|
177
|
+
"shop.order.problem": (0.5, 10),
|
|
178
|
+
"shop.order.receive": (0.5, 10),
|
|
179
|
+
"shop.quote": (2.0, 20),
|
|
180
|
+
"shop.report.dismiss": (0.5, 10),
|
|
181
|
+
"shop.report.list": (1.0, 12),
|
|
182
|
+
"shop.settings.update": (0.2, 6),
|
|
183
|
+
"shop.status": (2.0, 20),
|
|
184
|
+
"shop.wallet.bind": (0.2, 6),
|
|
185
|
+
"shop.wallet.list": (2.0, 20),
|
|
186
|
+
"shop.wallet.unlink": (0.2, 6),
|
|
187
|
+
"staff.override.set": (2.0, 20),
|
|
188
|
+
"staff.overrides": (2.0, 10),
|
|
189
|
+
"stage.hand.raise": (1.0, 4),
|
|
190
|
+
"stage.open": (1.0, 5),
|
|
191
|
+
"subscription.confirm": (0.3, 8),
|
|
192
|
+
"subscription.gift": (0.1, 5),
|
|
193
|
+
"subscription.invoice": (0.05, 6),
|
|
194
|
+
"subscription.status": (2.0, 20),
|
|
195
|
+
"ticket.list": (1.0, 5),
|
|
196
|
+
"ticket.open": (0.1, 3),
|
|
197
|
+
"ticket.reopen": (0.2, 3),
|
|
198
|
+
"user.get": (4.0, 20),
|
|
199
|
+
"user.report": (0.2, 5),
|
|
200
|
+
"verify.complete": (0.2, 10),
|
|
201
|
+
"verify.start": (0.02, 5),
|
|
202
|
+
"verify.status": (2.0, 20),
|
|
203
|
+
"voice.call.start": (1.0, 3),
|
|
204
|
+
"voice.camera.start": (1.0, 5),
|
|
205
|
+
"voice.join": (2.0, 10),
|
|
206
|
+
"voice.member.deafen": (4.0, 12),
|
|
207
|
+
"voice.member.disconnect": (4.0, 12),
|
|
208
|
+
"voice.member.move": (4.0, 12),
|
|
209
|
+
"voice.member.mute": (4.0, 12),
|
|
210
|
+
"voice.mls.commit": (5.0, 20),
|
|
211
|
+
"voice.mls.join": (2.0, 10),
|
|
212
|
+
"voice.mls.ready": (5.0, 20),
|
|
213
|
+
"voice.resume": (1.0, 5),
|
|
214
|
+
"voice.share.start": (1.0, 5),
|
|
215
|
+
"voice.sound.play": (0.5, 3),
|
|
216
|
+
"voice.turn.refresh": (0.1, 3),
|
|
217
|
+
"voice.watch": (4.0, 12),
|
|
218
|
+
"wallet.allowance": (1.0, 6),
|
|
219
|
+
"wallet.balances": (0.5, 5),
|
|
220
|
+
"wallet.broadcast": (0.2, 4),
|
|
221
|
+
"wallet.coin": (1.0, 6),
|
|
222
|
+
"wallet.fee": (1.0, 5),
|
|
223
|
+
"wallet.find_coins": (2.0, 8),
|
|
224
|
+
"wallet.forget_address": (0.5, 10),
|
|
225
|
+
"wallet.networks": (1.0, 5),
|
|
226
|
+
"wallet.nfts": (0.3, 4),
|
|
227
|
+
"wallet.plan_bitcoin": (0.5, 6),
|
|
228
|
+
"wallet.portfolio": (0.2, 3),
|
|
229
|
+
"wallet.prepare": (0.5, 6),
|
|
230
|
+
"wallet.prices": (1.0, 10),
|
|
231
|
+
"wallet.rates": (1.0, 5),
|
|
232
|
+
"wallet.receipt": (1.0, 10),
|
|
233
|
+
"wallet.register_address": (0.5, 10),
|
|
234
|
+
"wallet.simulate": (0.5, 6),
|
|
235
|
+
"wallet.swap_build": (0.3, 4),
|
|
236
|
+
"wallet.swap_quote": (0.5, 6),
|
|
237
|
+
"wallet.swap_status": (1.0, 3),
|
|
238
|
+
"wallet.swap_targets": (1.0, 6),
|
|
239
|
+
"wallet.tokens": (1.0, 10),
|
|
240
|
+
}
|
illest/_version.py
ADDED
illest/abc.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""The abstract bases: `Snowflake`, `Messageable`, `GuildChannel`, `User`, and the typing indicator.
|
|
2
|
+
|
|
3
|
+
What ILLEST does with each of `Messageable.send`'s keywords:
|
|
4
|
+
|
|
5
|
+
- `content`, `embed`/`embeds`, `view`, `reference`, `mention_author`: as their names say.
|
|
6
|
+
- `file`/`files`: each is sealed on this machine with a key of its own and uploaded as ciphertext; the key travels inside the
|
|
7
|
+
message, so only the conversation can open it.
|
|
8
|
+
- `allowed_mentions`: `<@id>`, `<@&id>` and `<#id>` in the text are written out as the names ILLEST shows (`@name`, `#channel`),
|
|
9
|
+
and the people named are notified unless `allowed_mentions` says not to.
|
|
10
|
+
- `delete_after`: the message is taken back after that many seconds, by this bot.
|
|
11
|
+
- `silent`: nobody is notified.
|
|
12
|
+
- `tts`, `stickers`, `nonce`, `suppress_embeds`, `poll`: `tts` and `stickers` are accepted and ignored (ILLEST has neither); `nonce`
|
|
13
|
+
is the message's own name for a retried send (`client_id`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import datetime as _dt
|
|
20
|
+
import re
|
|
21
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Protocol, Sequence, runtime_checkable
|
|
22
|
+
|
|
23
|
+
from .object import as_id, snowflake_time, wire_id
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .client import Client
|
|
27
|
+
from .embeds import Embed
|
|
28
|
+
from .file import File
|
|
29
|
+
from .mentions import AllowedMentions
|
|
30
|
+
from .models import Message
|
|
31
|
+
from .ui.view import View
|
|
32
|
+
|
|
33
|
+
__all__ = ("Snowflake", "Messageable", "Typing", "User", "GuildChannel", "PrivateChannel", "Connectable")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class Snowflake(Protocol):
|
|
38
|
+
id: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class User(Protocol):
|
|
43
|
+
id: int
|
|
44
|
+
name: str
|
|
45
|
+
bot: bool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class PrivateChannel(Protocol):
|
|
50
|
+
id: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Connectable(Protocol):
|
|
54
|
+
"""A voice channel. A bot has no microphone on ILLEST (calls are encrypted per device), so `connect` is not offered."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_USER = re.compile(r"<@!?(\d+)>")
|
|
58
|
+
_ROLE = re.compile(r"<@&(\d+)>")
|
|
59
|
+
_CHANNEL = re.compile(r"<#(\d+)>")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Typing:
|
|
63
|
+
"""`async with channel.typing():` shows the bot as typing until the block ends; `await channel.typing()` shows it once."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, messageable: "Messageable"):
|
|
66
|
+
self.messageable = messageable
|
|
67
|
+
self._task: asyncio.Task | None = None
|
|
68
|
+
|
|
69
|
+
async def _once(self) -> None:
|
|
70
|
+
client = self.messageable._state
|
|
71
|
+
await client.send_typing(self.messageable._channel_wire_id())
|
|
72
|
+
|
|
73
|
+
async def _keep(self) -> None:
|
|
74
|
+
while True:
|
|
75
|
+
await self._once()
|
|
76
|
+
await asyncio.sleep(5)
|
|
77
|
+
|
|
78
|
+
def __await__(self):
|
|
79
|
+
return self._once().__await__()
|
|
80
|
+
|
|
81
|
+
async def __aenter__(self) -> None:
|
|
82
|
+
await self._once()
|
|
83
|
+
self._task = asyncio.get_running_loop().create_task(self._keep_after_first())
|
|
84
|
+
|
|
85
|
+
async def _keep_after_first(self) -> None:
|
|
86
|
+
await asyncio.sleep(5)
|
|
87
|
+
await self._keep()
|
|
88
|
+
|
|
89
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
90
|
+
if self._task is not None:
|
|
91
|
+
self._task.cancel()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Messageable:
|
|
95
|
+
"""Anything a message can be sent to: a channel, a thread, a direct conversation, a member or user (through their DM)."""
|
|
96
|
+
|
|
97
|
+
_state: "Client"
|
|
98
|
+
|
|
99
|
+
async def _get_channel(self) -> "Messageable":
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def _channel_wire_id(self) -> str:
|
|
103
|
+
return wire_id(self.id) # type: ignore[attr-defined]
|
|
104
|
+
|
|
105
|
+
def _thread_root(self) -> str | None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
async def send(
|
|
109
|
+
self,
|
|
110
|
+
content: Any = None,
|
|
111
|
+
*,
|
|
112
|
+
tts: bool = False,
|
|
113
|
+
embed: "Embed | None" = None,
|
|
114
|
+
embeds: Sequence["Embed"] | None = None,
|
|
115
|
+
file: "File | None" = None,
|
|
116
|
+
files: Sequence["File"] | None = None,
|
|
117
|
+
stickers: Sequence[Any] | None = None,
|
|
118
|
+
delete_after: float | None = None,
|
|
119
|
+
nonce: str | int | None = None,
|
|
120
|
+
allowed_mentions: "AllowedMentions | None" = None,
|
|
121
|
+
reference: Any = None,
|
|
122
|
+
mention_author: bool | None = None,
|
|
123
|
+
view: "View | None" = None,
|
|
124
|
+
suppress_embeds: bool = False,
|
|
125
|
+
silent: bool = False,
|
|
126
|
+
poll: Any = None,
|
|
127
|
+
ephemeral_for: Any = None,
|
|
128
|
+
ask_reply: Any = None,
|
|
129
|
+
) -> "Message":
|
|
130
|
+
"""Send a message here. `ask_reply` (True, a placeholder, or an `AskReply`) opens the reply box on it, so the answer comes
|
|
131
|
+
back as a reply."""
|
|
132
|
+
channel = await self._get_channel()
|
|
133
|
+
return await channel._state._send_to(
|
|
134
|
+
channel._channel_wire_id(),
|
|
135
|
+
content,
|
|
136
|
+
ask_reply=ask_reply,
|
|
137
|
+
thread_root=channel._thread_root(),
|
|
138
|
+
embed=embed,
|
|
139
|
+
embeds=embeds,
|
|
140
|
+
file=file,
|
|
141
|
+
files=files,
|
|
142
|
+
delete_after=delete_after,
|
|
143
|
+
nonce=nonce,
|
|
144
|
+
allowed_mentions=allowed_mentions,
|
|
145
|
+
reference=reference,
|
|
146
|
+
mention_author=mention_author,
|
|
147
|
+
view=view,
|
|
148
|
+
suppress_embeds=suppress_embeds,
|
|
149
|
+
silent=silent,
|
|
150
|
+
ephemeral_for=ephemeral_for,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def typing(self) -> Typing:
|
|
154
|
+
return Typing(self)
|
|
155
|
+
|
|
156
|
+
async def trigger_typing(self) -> None:
|
|
157
|
+
await self.typing()
|
|
158
|
+
|
|
159
|
+
async def fetch_message(self, id: int) -> "Message": # noqa: A002
|
|
160
|
+
"""A message this bot holds. An end-to-end encrypted message is readable only once, as it arrives; the bot keeps the newest
|
|
161
|
+
`max_messages` (and, with `Client(message_store=...)`, writes them to disk), and that is what it can fetch."""
|
|
162
|
+
channel = await self._get_channel()
|
|
163
|
+
found = channel._state._connection_message(as_id(id))
|
|
164
|
+
from .errors import NotFound
|
|
165
|
+
|
|
166
|
+
if found is None or found.channel.id != channel.id:
|
|
167
|
+
raise NotFound("NOT_FOUND", "this bot does not hold that message (it reads a message once, as it arrives)")
|
|
168
|
+
return found
|
|
169
|
+
|
|
170
|
+
def history(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
limit: int | None = 100,
|
|
174
|
+
before: Any = None,
|
|
175
|
+
after: Any = None,
|
|
176
|
+
around: Any = None,
|
|
177
|
+
oldest_first: bool | None = None,
|
|
178
|
+
) -> AsyncIterator["Message"]:
|
|
179
|
+
"""The messages this bot holds for this channel, newest first (oldest first when `after` is given).
|
|
180
|
+
|
|
181
|
+
ILLEST's server holds only ciphertext, and a message can be opened once, by the devices that were in the conversation when it
|
|
182
|
+
was sent. So the history a bot can read is what it has read: kept in memory (`max_messages`) and, when the bot was made
|
|
183
|
+
with `message_store=`, on disk.
|
|
184
|
+
"""
|
|
185
|
+
return _history(self, limit, before, after, around, oldest_first)
|
|
186
|
+
|
|
187
|
+
async def pins(self) -> list["Message"]:
|
|
188
|
+
channel = await self._get_channel()
|
|
189
|
+
answer = await channel._state.request("channel.pins.list", {"channel_id": channel._channel_wire_id()})
|
|
190
|
+
out = []
|
|
191
|
+
for raw in answer.get("pins") or answer.get("messages") or []:
|
|
192
|
+
message_id = raw.get("message_id") if isinstance(raw, dict) else raw
|
|
193
|
+
held = channel._state._connection_message(as_id(message_id)) if message_id else None
|
|
194
|
+
if held is not None:
|
|
195
|
+
out.append(held)
|
|
196
|
+
return out
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _instant(value: Any) -> float | None:
|
|
200
|
+
if value is None:
|
|
201
|
+
return None
|
|
202
|
+
if isinstance(value, _dt.datetime):
|
|
203
|
+
return value.timestamp()
|
|
204
|
+
return snowflake_time(as_id(value)).timestamp()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def _history(channel: Messageable, limit, before, after, around, oldest_first) -> AsyncIterator["Message"]:
|
|
208
|
+
target = await channel._get_channel()
|
|
209
|
+
held = target._state._messages_in(as_id(target.id), target._thread_root())
|
|
210
|
+
before_t, after_t, around_t = _instant(before), _instant(after), _instant(around)
|
|
211
|
+
rows = [m for m in held if (before_t is None or m.created_at.timestamp() < before_t) and (after_t is None or m.created_at.timestamp() > after_t)]
|
|
212
|
+
if around_t is not None:
|
|
213
|
+
rows.sort(key=lambda m: abs(m.created_at.timestamp() - around_t))
|
|
214
|
+
rows = rows[: limit or len(rows)]
|
|
215
|
+
if oldest_first is None:
|
|
216
|
+
oldest_first = after is not None
|
|
217
|
+
rows.sort(key=lambda m: m.id, reverse=not oldest_first)
|
|
218
|
+
if limit is not None and around_t is None:
|
|
219
|
+
rows = rows[:limit]
|
|
220
|
+
for message in rows:
|
|
221
|
+
yield message
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class GuildChannel:
|
|
225
|
+
"""Marker for a server's channel; the behaviour is `models.GuildChannel`'s."""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def rewrite_mentions(client: "Client", text: str, guild: Any = None) -> tuple[str, list[int]]:
|
|
229
|
+
"""Mention tokens written as ILLEST shows them, and who they name: `<@5>` becomes `@mira` (5 is notified),
|
|
230
|
+
`<@&9>` becomes `@Mods`, `<#12>` becomes `#general`. A token for somebody the bot cannot name is left as it is."""
|
|
231
|
+
named: list[int] = []
|
|
232
|
+
|
|
233
|
+
def user(match: re.Match) -> str:
|
|
234
|
+
uid = int(match.group(1))
|
|
235
|
+
known = client.get_user(uid)
|
|
236
|
+
if known is None:
|
|
237
|
+
return match.group(0)
|
|
238
|
+
if uid not in named:
|
|
239
|
+
named.append(uid)
|
|
240
|
+
return f"@{known.name}"
|
|
241
|
+
|
|
242
|
+
def role(match: re.Match) -> str:
|
|
243
|
+
rid = int(match.group(1))
|
|
244
|
+
for g in [guild] if guild is not None else client.guilds:
|
|
245
|
+
found = g.get_role(rid) if g is not None else None
|
|
246
|
+
if found is not None:
|
|
247
|
+
return f"@{found.name}"
|
|
248
|
+
return match.group(0)
|
|
249
|
+
|
|
250
|
+
def channel(match: re.Match) -> str:
|
|
251
|
+
found = client.get_channel(int(match.group(1)))
|
|
252
|
+
return f"#{found.name}" if found is not None and getattr(found, "name", None) else match.group(0)
|
|
253
|
+
|
|
254
|
+
text = _USER.sub(user, text)
|
|
255
|
+
text = _ROLE.sub(role, text)
|
|
256
|
+
text = _CHANNEL.sub(channel, text)
|
|
257
|
+
return text, named
|