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.
Files changed (65) hide show
  1. illest/__init__.py +169 -0
  2. illest/_limits.py +240 -0
  3. illest/_version.py +3 -0
  4. illest/abc.py +257 -0
  5. illest/activity.py +100 -0
  6. illest/app_commands/__init__.py +76 -0
  7. illest/app_commands/checks.py +161 -0
  8. illest/app_commands/commands.py +807 -0
  9. illest/app_commands/errors.py +130 -0
  10. illest/app_commands/namespace.py +31 -0
  11. illest/app_commands/tree.py +341 -0
  12. illest/attachment.py +161 -0
  13. illest/auth.py +286 -0
  14. illest/buttons.py +160 -0
  15. illest/client.py +2272 -0
  16. illest/colour.py +156 -0
  17. illest/content.py +470 -0
  18. illest/embeds.py +362 -0
  19. illest/enums.py +249 -0
  20. illest/envelope.py +97 -0
  21. illest/errors.py +165 -0
  22. illest/ext/__init__.py +1 -0
  23. illest/ext/commands/__init__.py +127 -0
  24. illest/ext/commands/bot.py +538 -0
  25. illest/ext/commands/cog.py +174 -0
  26. illest/ext/commands/converter.py +412 -0
  27. illest/ext/commands/core.py +1164 -0
  28. illest/ext/commands/errors.py +343 -0
  29. illest/ext/commands/flags.py +156 -0
  30. illest/ext/commands/help.py +382 -0
  31. illest/ext/commands/hybrid.py +153 -0
  32. illest/ext/tasks/__init__.py +391 -0
  33. illest/file.py +59 -0
  34. illest/gateway.py +513 -0
  35. illest/intents.py +190 -0
  36. illest/interactions.py +564 -0
  37. illest/media.py +182 -0
  38. illest/mentions.py +41 -0
  39. illest/mls.py +843 -0
  40. illest/models.py +2562 -0
  41. illest/object.py +70 -0
  42. illest/permissions.py +317 -0
  43. illest/pins.py +152 -0
  44. illest/py.typed +0 -0
  45. illest/ratelimit.py +113 -0
  46. illest/replies.py +34 -0
  47. illest/rpc/__init__.py +45 -0
  48. illest/rpc/_async.py +355 -0
  49. illest/rpc/_sync.py +164 -0
  50. illest/rpc/_wire.py +223 -0
  51. illest/rpc/errors.py +66 -0
  52. illest/ui/__init__.py +52 -0
  53. illest/ui/button.py +126 -0
  54. illest/ui/components.py +54 -0
  55. illest/ui/item.py +69 -0
  56. illest/ui/layout.py +417 -0
  57. illest/ui/modal.py +636 -0
  58. illest/ui/select.py +290 -0
  59. illest/ui/view.py +249 -0
  60. illest/utils.py +217 -0
  61. illest-2.2.0.dist-info/METADATA +151 -0
  62. illest-2.2.0.dist-info/RECORD +65 -0
  63. illest-2.2.0.dist-info/WHEEL +5 -0
  64. illest-2.2.0.dist-info/licenses/LICENSE +24 -0
  65. 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
@@ -0,0 +1,3 @@
1
+ """The version, in one place: `pyproject.toml` reads it from here."""
2
+
3
+ __version__ = "2.2.0"
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