hikari-risa 1.0.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.
risa/context.py ADDED
@@ -0,0 +1,693 @@
1
+ # Copyright (c) 2025 Rayakame
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+ """The contexts handlers and hooks answer interactions through."""
21
+
22
+ from __future__ import annotations
23
+
24
+ import asyncio
25
+ import enum
26
+ import typing
27
+
28
+ import hikari
29
+ import msgspec
30
+
31
+ from risa import errors
32
+ from risa.ui import build as build_
33
+
34
+ if typing.TYPE_CHECKING:
35
+ import collections.abc
36
+
37
+ from hikari.api import special_endpoints
38
+
39
+ from risa import ui
40
+ from risa import view as view_
41
+ from risa.internal import registry
42
+ from risa.state import backend
43
+
44
+ __all__ = (
45
+ "ClickFailure",
46
+ "ComponentContext",
47
+ "Context",
48
+ "ErrorHandler",
49
+ "FailureStage",
50
+ "Response",
51
+ "ResponseGate",
52
+ )
53
+
54
+ type ErrorHandler = collections.abc.Callable[
55
+ typing.Concatenate[ClickFailure, ...],
56
+ collections.abc.Awaitable[bool],
57
+ ]
58
+ """A function `Client.error_handler` registers: it takes a `ClickFailure`, then anything to inject."""
59
+
60
+
61
+ class FailureStage(enum.StrEnum):
62
+ """Where a click failed, as `ClickFailure.stage` names it.
63
+
64
+ Attributes
65
+ ----------
66
+ HANDLER
67
+ The handler raised.
68
+ LOAD
69
+ The view's `load` raised, so the handler never ran.
70
+ ON_OUTDATED
71
+ `View.on_outdated` raised.
72
+ ON_STATE_MISSING
73
+ `View.on_state_missing` raised.
74
+ STATE
75
+ The view's state could not be reached before any of the bot's code ran: the store
76
+ failed or is missing, its lock timed out, or the codec's extra is not installed.
77
+ """
78
+
79
+ HANDLER = enum.auto()
80
+ LOAD = enum.auto()
81
+ ON_OUTDATED = enum.auto()
82
+ ON_STATE_MISSING = enum.auto()
83
+ STATE = enum.auto()
84
+
85
+
86
+ class _InitialResponse(enum.StrEnum):
87
+ NONE = enum.auto()
88
+ DEFERRED_UPDATE = enum.auto()
89
+ DEFERRED_THINKING = enum.auto()
90
+ MESSAGE_CREATE = enum.auto()
91
+ MESSAGE_UPDATE = enum.auto()
92
+
93
+
94
+ class ResponseGate(msgspec.Struct):
95
+ """Whether an interaction has been answered, shared by everything that answers it.
96
+
97
+ risa makes one per interaction, so a handler and the auto-defer watchdog never both
98
+ send the initial response.
99
+
100
+ Attributes
101
+ ----------
102
+ lock
103
+ Held while the initial response is decided and sent.
104
+ acknowledged
105
+ Set once nothing more is owed to the REST listener: the initial response went out,
106
+ or risa has finished with the interaction.
107
+ response
108
+ Which initial response the interaction got.
109
+ adopted
110
+ Whether the click routed to a view, rather than being left to another listener.
111
+ """
112
+
113
+ lock: asyncio.Lock = msgspec.field(default_factory=asyncio.Lock)
114
+ acknowledged: asyncio.Event = msgspec.field(default_factory=asyncio.Event)
115
+ response: _InitialResponse = msgspec.field(default=_InitialResponse.NONE)
116
+ adopted: bool = False
117
+
118
+ @property
119
+ def responded(self) -> bool:
120
+ """Whether the interaction has its initial response."""
121
+ return self.response is not _InitialResponse.NONE
122
+
123
+
124
+ class ViewSlot(msgspec.Struct):
125
+ view: view_.View | None = None
126
+ session: backend.StateSession | None = None
127
+ overflowed: set[bytes] = msgspec.field(default_factory=set[bytes])
128
+ replaced: bool = False
129
+
130
+
131
+ class Response:
132
+ """A message sent with `respond()`, to fetch, edit or delete later."""
133
+
134
+ __slots__ = ("_interaction", "_message", "_rest")
135
+
136
+ def __init__(
137
+ self,
138
+ rest: hikari.api.RESTClient,
139
+ interaction: hikari.ComponentInteraction | hikari.ModalInteraction,
140
+ message: hikari.Message | None,
141
+ ) -> None:
142
+ self._rest = rest
143
+ self._interaction = interaction
144
+ self._message = message
145
+
146
+ async def fetch(self) -> hikari.Message:
147
+ """Fetch the message as it is now."""
148
+ if self._message is not None:
149
+ return await self._rest.fetch_webhook_message(
150
+ self._interaction.application_id,
151
+ self._interaction.token,
152
+ self._message.id,
153
+ )
154
+ return await self._rest.fetch_interaction_response(self._interaction.application_id, self._interaction.token)
155
+
156
+ async def delete(self) -> None:
157
+ """Delete the message."""
158
+ if self._message is not None:
159
+ await self._rest.delete_webhook_message(
160
+ self._interaction.application_id,
161
+ self._interaction.token,
162
+ self._message.id,
163
+ )
164
+ else:
165
+ await self._rest.delete_interaction_response(self._interaction.application_id, self._interaction.token)
166
+
167
+ async def edit( # ruff:ignore[too-many-arguments]
168
+ self,
169
+ content: hikari.UndefinedNoneOr[str] = hikari.UNDEFINED,
170
+ *,
171
+ attachment: hikari.UndefinedNoneOr[hikari.Resourceish | hikari.Attachment] = hikari.UNDEFINED,
172
+ attachments: hikari.UndefinedNoneOr[
173
+ collections.abc.Sequence[hikari.Resourceish | hikari.Attachment]
174
+ ] = hikari.UNDEFINED,
175
+ component: hikari.UndefinedNoneOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
176
+ components: hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
177
+ embed: hikari.UndefinedNoneOr[hikari.Embed] = hikari.UNDEFINED,
178
+ embeds: hikari.UndefinedNoneOr[collections.abc.Sequence[hikari.Embed]] = hikari.UNDEFINED,
179
+ mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
180
+ user_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialUser] | bool] = hikari.UNDEFINED,
181
+ role_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialRole] | bool] = hikari.UNDEFINED,
182
+ ) -> hikari.Message:
183
+ """Edit the message.
184
+
185
+ The parameters are those of hikari's
186
+ [`edit_interaction_response`][hikari.api.rest.RESTClient.edit_interaction_response], which
187
+ describes them in full. Every edit sends the mention settings, so one left out switches
188
+ that mention parsing off.
189
+
190
+ Parameters
191
+ ----------
192
+ content
193
+ The new text. `None` removes it.
194
+ attachment
195
+ One file. Not together with `attachments`.
196
+ attachments
197
+ The files to keep or add. `None` removes them all.
198
+ component
199
+ One builder, sent as it is: risa adds no routing or state. Not together with
200
+ `components`.
201
+ components
202
+ The builders, sent as they are. `None` or an empty sequence removes them.
203
+ embed
204
+ One embed. Not together with `embeds`.
205
+ embeds
206
+ The embeds. `None` or an empty sequence removes them.
207
+ mentions_everyone
208
+ Whether `@everyone` and `@here` are parsed.
209
+ user_mentions
210
+ `True` for every user, or the users to parse.
211
+ role_mentions
212
+ `True` for every role, or the roles to parse.
213
+
214
+ Returns
215
+ -------
216
+ :
217
+ The message as it is now.
218
+ """
219
+ if self._message is not None:
220
+ return await self._rest.edit_webhook_message(
221
+ self._interaction.application_id,
222
+ self._interaction.token,
223
+ self._message.id,
224
+ content,
225
+ attachment=attachment,
226
+ attachments=attachments,
227
+ component=component,
228
+ components=components,
229
+ embed=embed,
230
+ embeds=embeds,
231
+ mentions_everyone=mentions_everyone,
232
+ user_mentions=user_mentions,
233
+ role_mentions=role_mentions,
234
+ )
235
+ return await self._rest.edit_interaction_response(
236
+ self._interaction.application_id,
237
+ self._interaction.token,
238
+ content,
239
+ attachment=attachment,
240
+ attachments=attachments,
241
+ component=component,
242
+ components=components,
243
+ embed=embed,
244
+ embeds=embeds,
245
+ mentions_everyone=mentions_everyone,
246
+ user_mentions=user_mentions,
247
+ role_mentions=role_mentions,
248
+ )
249
+
250
+
251
+ class Context[T: hikari.ComponentInteraction | hikari.ModalInteraction]:
252
+ """What a handler or hook gets for the interaction it answers."""
253
+
254
+ __slots__ = ("_gate", "_interaction", "_rest")
255
+
256
+ def __init__(self, interaction: T, *, rest: hikari.api.RESTClient, gate: ResponseGate | None = None) -> None:
257
+ self._interaction = interaction
258
+ self._rest = rest
259
+ self._gate = gate if gate is not None else ResponseGate()
260
+
261
+ @property
262
+ def responded(self) -> bool:
263
+ """Whether the interaction has its initial response."""
264
+ return self._gate.responded
265
+
266
+ @property
267
+ def rest(self) -> hikari.api.RESTClient:
268
+ """The REST client risa answers through."""
269
+ return self._rest
270
+
271
+ @property
272
+ def interaction(self) -> T:
273
+ """The interaction being answered."""
274
+ return self._interaction
275
+
276
+ @property
277
+ def custom_id(self) -> str:
278
+ """The `custom_id` of the component that was used."""
279
+ return self._interaction.custom_id
280
+
281
+ @property
282
+ def user(self) -> hikari.User:
283
+ """The user who used the component."""
284
+ return self._interaction.user
285
+
286
+ @property
287
+ def member(self) -> hikari.InteractionMember | None:
288
+ """The member who used the component, or `None` outside a guild."""
289
+ return self._interaction.member
290
+
291
+ @property
292
+ def channel_id(self) -> hikari.Snowflake:
293
+ """The id of the channel the interaction happened in."""
294
+ return self._interaction.channel_id
295
+
296
+ @property
297
+ def guild_id(self) -> hikari.Snowflake | None:
298
+ """The id of the guild the interaction happened in, or `None` outside a guild."""
299
+ return self._interaction.guild_id
300
+
301
+ @property
302
+ def message(self) -> hikari.Message | None:
303
+ """The message the component is on, if any."""
304
+ return self._interaction.message
305
+
306
+ async def defer(self, *, thinking: bool = False, ephemeral: bool = False) -> None:
307
+ """Acknowledge the interaction now and answer it later.
308
+
309
+ By default the acknowledgement is silent, and the answer is a `rerender()` or an
310
+ `edit()` of the message. With `thinking=True`, Discord shows a "thinking..." message
311
+ that the next `respond()` takes the place of.
312
+
313
+ Parameters
314
+ ----------
315
+ thinking
316
+ Show a "thinking..." message.
317
+ ephemeral
318
+ Show that message only to the user who clicked.
319
+
320
+ Raises
321
+ ------
322
+ AlreadyRespondedError
323
+ If the interaction already has its initial response.
324
+ """
325
+ async with self._gate.lock:
326
+ if self._gate.response is not _InitialResponse.NONE:
327
+ attempted = "defer"
328
+ raise errors.AlreadyRespondedError(attempted, self._gate.response.name)
329
+ await self._rest.create_interaction_response(
330
+ self._interaction.id,
331
+ self._interaction.token,
332
+ hikari.ResponseType.DEFERRED_MESSAGE_CREATE
333
+ if thinking
334
+ else hikari.ResponseType.DEFERRED_MESSAGE_UPDATE,
335
+ flags=hikari.MessageFlag.EPHEMERAL if ephemeral else hikari.UNDEFINED,
336
+ )
337
+ self._record_initial(_InitialResponse.DEFERRED_THINKING if thinking else _InitialResponse.DEFERRED_UPDATE)
338
+
339
+ async def acknowledge(self, *, thinking: bool = False, ephemeral: bool = False) -> None:
340
+ """Acknowledge the interaction like `defer`, unless it already has its initial response.
341
+
342
+ Where `defer` raises, this does nothing, so it is safe to call whatever happened
343
+ before.
344
+
345
+ Parameters
346
+ ----------
347
+ thinking
348
+ Show a "thinking..." message.
349
+ ephemeral
350
+ Show that message only to the user who clicked.
351
+ """
352
+ async with self._gate.lock:
353
+ if self._gate.response is not _InitialResponse.NONE:
354
+ return
355
+ await self._rest.create_interaction_response(
356
+ self._interaction.id,
357
+ self._interaction.token,
358
+ hikari.ResponseType.DEFERRED_MESSAGE_CREATE
359
+ if thinking
360
+ else hikari.ResponseType.DEFERRED_MESSAGE_UPDATE,
361
+ flags=hikari.MessageFlag.EPHEMERAL if ephemeral else hikari.UNDEFINED,
362
+ )
363
+ self._record_initial(_InitialResponse.DEFERRED_THINKING if thinking else _InitialResponse.DEFERRED_UPDATE)
364
+
365
+ async def respond( # ruff:ignore[too-many-arguments]
366
+ self,
367
+ content: hikari.UndefinedOr[str] = hikari.UNDEFINED,
368
+ *,
369
+ ephemeral: bool = False,
370
+ tts: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
371
+ attachment: hikari.UndefinedOr[hikari.Resourceish] = hikari.UNDEFINED,
372
+ attachments: hikari.UndefinedOr[collections.abc.Sequence[hikari.Resourceish]] = hikari.UNDEFINED,
373
+ component: hikari.UndefinedOr[hikari.api.ComponentBuilder] = hikari.UNDEFINED,
374
+ components: hikari.UndefinedOr[collections.abc.Sequence[hikari.api.ComponentBuilder]] = hikari.UNDEFINED,
375
+ embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED,
376
+ embeds: hikari.UndefinedOr[collections.abc.Sequence[hikari.Embed]] = hikari.UNDEFINED,
377
+ mentions_everyone: hikari.UndefinedOr[bool] = hikari.UNDEFINED,
378
+ user_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialUser] | bool] = hikari.UNDEFINED,
379
+ role_mentions: hikari.UndefinedOr[hikari.SnowflakeishSequence[hikari.PartialRole] | bool] = hikari.UNDEFINED,
380
+ ) -> Response:
381
+ """Send a message in answer to the interaction.
382
+
383
+ The first response answers the interaction itself; every later one is a follow-up
384
+ message. After a thinking message, the first one replaces it, and keeps its visibility
385
+ whatever `ephemeral` and `tts` say. Apart from `ephemeral`, the parameters are those of hikari's
386
+ [`create_interaction_response`][hikari.api.rest.RESTClient.create_interaction_response]
387
+ and [`execute_webhook`][hikari.api.rest.RESTClient.execute_webhook], which describe them
388
+ in full.
389
+
390
+ Parameters
391
+ ----------
392
+ content
393
+ The message text.
394
+ ephemeral
395
+ Show the message only to the user who clicked.
396
+ tts
397
+ Have Discord read the message aloud.
398
+ attachment
399
+ One file. Not together with `attachments`.
400
+ attachments
401
+ The files to attach.
402
+ component
403
+ One builder, sent as it is: risa adds no routing or state. To send a view, spread
404
+ `await client.build(view)`. Not together with `components`.
405
+ components
406
+ The builders, sent as they are.
407
+ embed
408
+ One embed. Not together with `embeds`.
409
+ embeds
410
+ The embeds.
411
+ mentions_everyone
412
+ Whether `@everyone` and `@here` are parsed.
413
+ user_mentions
414
+ `True` for every user, or the users to parse.
415
+ role_mentions
416
+ `True` for every role, or the roles to parse.
417
+
418
+ Returns
419
+ -------
420
+ :
421
+ The message, to fetch, edit or delete later.
422
+ """
423
+ flags = hikari.MessageFlag.EPHEMERAL if ephemeral else hikari.UNDEFINED
424
+
425
+ async with self._gate.lock:
426
+ if self._gate.response is _InitialResponse.NONE:
427
+ await self._rest.create_interaction_response(
428
+ self._interaction.id,
429
+ self._interaction.token,
430
+ hikari.ResponseType.MESSAGE_CREATE,
431
+ content,
432
+ flags=flags,
433
+ tts=tts,
434
+ attachment=attachment,
435
+ attachments=attachments,
436
+ component=component,
437
+ components=components,
438
+ embed=embed,
439
+ embeds=embeds,
440
+ mentions_everyone=mentions_everyone,
441
+ user_mentions=user_mentions,
442
+ role_mentions=role_mentions,
443
+ )
444
+ self._record_initial(_InitialResponse.MESSAGE_CREATE)
445
+ return Response(self._rest, self._interaction, None)
446
+
447
+ if self._gate.response is _InitialResponse.DEFERRED_THINKING:
448
+ await self._rest.edit_interaction_response(
449
+ self._interaction.application_id,
450
+ self._interaction.token,
451
+ content,
452
+ attachment=attachment,
453
+ attachments=attachments,
454
+ component=component,
455
+ components=components,
456
+ embed=embed,
457
+ embeds=embeds,
458
+ mentions_everyone=mentions_everyone,
459
+ user_mentions=user_mentions,
460
+ role_mentions=role_mentions,
461
+ )
462
+ self._record_initial(_InitialResponse.MESSAGE_CREATE)
463
+ return Response(self._rest, self._interaction, None)
464
+
465
+ message = await self._rest.execute_webhook(
466
+ self._interaction.application_id,
467
+ self._interaction.token,
468
+ content,
469
+ flags=flags,
470
+ tts=tts,
471
+ attachment=attachment,
472
+ attachments=attachments,
473
+ component=component,
474
+ components=components,
475
+ embed=embed,
476
+ embeds=embeds,
477
+ mentions_everyone=mentions_everyone,
478
+ user_mentions=user_mentions,
479
+ role_mentions=role_mentions,
480
+ )
481
+ return Response(self._rest, self._interaction, message)
482
+
483
+ def _record_initial(self, response: _InitialResponse) -> None:
484
+ self._gate.response = response
485
+ self._gate.acknowledged.set()
486
+
487
+
488
+ class ComponentContext(Context[hikari.ComponentInteraction]):
489
+ """The context of a click on one of a view's components.
490
+
491
+ A handler answers through it: `rerender()` redraws the message from the view's
492
+ state, `edit(...)` replaces it with another layout, and `respond(...)` sends a new
493
+ message. risa makes one per click.
494
+ """
495
+
496
+ __slots__ = ("_build", "_meta", "_slot")
497
+
498
+ def __init__(
499
+ self,
500
+ interaction: hikari.ComponentInteraction,
501
+ *,
502
+ rest: hikari.api.RESTClient,
503
+ meta: registry.ViewMeta,
504
+ build: collections.abc.Callable[[view_.View, hikari.Snowflake], collections.abc.Awaitable[build_.Rendered]],
505
+ gate: ResponseGate | None = None,
506
+ slot: ViewSlot | None = None,
507
+ ) -> None:
508
+ super().__init__(interaction, rest=rest, gate=gate)
509
+ self._meta = meta
510
+ self._build = build
511
+ self._slot = slot if slot is not None else ViewSlot()
512
+
513
+ @property
514
+ @typing.override
515
+ def message(self) -> hikari.Message:
516
+ return self._interaction.message
517
+
518
+ @property
519
+ def values(self) -> collections.abc.Sequence[str]:
520
+ """The values picked in a select menu; empty for a button."""
521
+ return self._interaction.values
522
+
523
+ @property
524
+ def resolved(self) -> hikari.ResolvedOptionData | None:
525
+ """The users, members, roles and channels picked in an entity select menu, or `None`."""
526
+ return self._interaction.resolved
527
+
528
+ async def edit(self, layout: ui.Layout, /) -> None:
529
+ """Replace the message with `layout`, keeping the view's state on it.
530
+
531
+ For a screen the view's `render` does not draw, such as a confirmation or a closing
532
+ message. A layout without interactive components ends the view, since nothing on it
533
+ can be clicked any more. Like `rerender()`, it saves the view's state.
534
+
535
+ Parameters
536
+ ----------
537
+ layout
538
+ The new layout: one top-level component, or several in order.
539
+
540
+ Raises
541
+ ------
542
+ NoViewError
543
+ If the layout has interactive components and the view has durable fields, but this
544
+ context has no view state, as in `on_outdated` and `on_state_missing`, or if
545
+ `replace()` gave the message to another view.
546
+ EphemeralOriginError
547
+ If the message is ephemeral and the interaction was answered with a new message.
548
+ StateOverflowError
549
+ If an `InMessage` view's state does not fit the layout's components.
550
+ StateConflictError
551
+ If another process saved the view's state first. The view then holds that state.
552
+ StateUnreadableError
553
+ If another process saved the view's state first, in a form this code cannot read.
554
+ """
555
+ if self._slot.replaced:
556
+ raise errors.NoViewError(self._meta.name, "edit() after replace()")
557
+ plan = build_.plan(layout, self._meta)
558
+ view, session = self._slot.view, self._slot.session
559
+ if view is not None and session is not None:
560
+ await self._commit(view, session, plan)
561
+ return
562
+ if plan.interactive and self._meta.schema.durable:
563
+ raise errors.NoViewError(self._meta.name, "edit() with interactive components")
564
+ await self._update_origin(plan.emit(b""))
565
+
566
+ async def replace(self, view: view_.View, /) -> None:
567
+ """Turn the message into another view, which answers its clicks from here on.
568
+
569
+ It builds `view` as `Client.build` does, so a view in a store gets a record of its own.
570
+ This view's state is not saved, also not when the handler returns, and its record stays
571
+ until its `ttl` runs out. If the message cannot be edited, the new view's record stays
572
+ the same way and this view keeps the message. Afterwards `rerender()` and `edit()` of
573
+ this context raise `NoViewError`, since the message is no longer this view's; `edit`
574
+ only ever shows this view's own components.
575
+
576
+ Parameters
577
+ ----------
578
+ view
579
+ The view to show, an instance of a registered class.
580
+
581
+ Raises
582
+ ------
583
+ EphemeralOriginError
584
+ If the message is ephemeral and the interaction was answered with a new message.
585
+ NotAViewError
586
+ If the view's class was never registered.
587
+ StateOverflowError
588
+ If an `InMessage` view's state does not fit its components.
589
+ ViewDeclarationError
590
+ If the view keeps its state in a store this client does not have.
591
+ """
592
+ rendered = await self._build(view, self.message.id)
593
+ await self._update_origin(rendered.components)
594
+ self._slot.view = None
595
+ self._slot.session = None
596
+ self._slot.replaced = True
597
+
598
+ async def rerender(self) -> None:
599
+ """Redraw the message from the view's current state, and save that state.
600
+
601
+ Every rerender is a commit. `InMessage` state is written into the message itself;
602
+ `InStore` state goes to the store first, then the message is edited. A change made
603
+ without calling it is saved when the handler returns, but that save can only log a
604
+ failure; call it yourself, before any `respond()`, where a failure must be handled.
605
+
606
+ Raises
607
+ ------
608
+ NoViewError
609
+ If the view has durable fields but this context has no view state, as in
610
+ `on_outdated` and `on_state_missing`, or if `replace()` gave the message to
611
+ another view.
612
+ EphemeralOriginError
613
+ If the message is ephemeral and the interaction was answered with a new message.
614
+ StateOverflowError
615
+ If an `InMessage` view's state no longer fits its components.
616
+ StateConflictError
617
+ If another process saved the view's state first. The view then holds that state,
618
+ so the handler can apply its change again and rerender.
619
+ StateUnreadableError
620
+ If another process saved the view's state first, in a form this code cannot read.
621
+ StateNotFoundError
622
+ If an `InStore` view's record disappeared while the handler ran.
623
+ """
624
+ if self._slot.replaced:
625
+ raise errors.NoViewError(self._meta.name, "rerender() after replace()")
626
+ view, session = self._slot.view, self._slot.session
627
+ if view is not None and session is not None:
628
+ await self._commit(view, session, build_.plan(view.render(), self._meta))
629
+ return
630
+ if self._meta.schema.durable:
631
+ raise errors.NoViewError(self._meta.name, "rerender()")
632
+ await self._update_origin(build_.plan(self._meta.cls().render(), self._meta).emit(b""))
633
+
634
+ async def _commit(self, view: view_.View, session: backend.StateSession, plan: build_.Plan) -> None:
635
+ if self._origin_is_out_of_reach():
636
+ raise errors.EphemeralOriginError(self._meta.name)
637
+ state = await session.stage(view)
638
+ try:
639
+ builders = plan.emit(state)
640
+ except errors.StateOverflowError:
641
+ self._slot.overflowed.add(self._meta.schema.pack_view(view))
642
+ raise
643
+ await self._update_origin(builders)
644
+ await session.published(view)
645
+
646
+ async def _update_origin(self, builders: collections.abc.Sequence[special_endpoints.ComponentBuilder]) -> None:
647
+ async with self._gate.lock:
648
+ if self._gate.response is _InitialResponse.NONE:
649
+ await self._rest.create_interaction_response(
650
+ self._interaction.id,
651
+ self._interaction.token,
652
+ hikari.ResponseType.MESSAGE_UPDATE,
653
+ components=builders,
654
+ )
655
+ self._record_initial(_InitialResponse.MESSAGE_UPDATE)
656
+ elif self._gate.response in {_InitialResponse.DEFERRED_UPDATE, _InitialResponse.MESSAGE_UPDATE}:
657
+ await self._rest.edit_interaction_response(
658
+ self._interaction.application_id,
659
+ self._interaction.token,
660
+ components=builders,
661
+ )
662
+ else:
663
+ if self._origin_is_out_of_reach():
664
+ raise errors.EphemeralOriginError(self._meta.name)
665
+ await self._rest.edit_message(self.message.channel_id, self.message.id, components=builders)
666
+
667
+ def _origin_is_out_of_reach(self) -> bool:
668
+ answered_elsewhere = self._gate.response in {
669
+ _InitialResponse.DEFERRED_THINKING,
670
+ _InitialResponse.MESSAGE_CREATE,
671
+ }
672
+ return answered_elsewhere and hikari.MessageFlag.EPHEMERAL in self.message.flags
673
+
674
+
675
+ class ClickFailure(msgspec.Struct, frozen=True):
676
+ """A click that failed, as the functions `Client.error_handler` registers receive it.
677
+
678
+ Attributes
679
+ ----------
680
+ ctx
681
+ The context of the click. It has no view state, as in `View.on_outdated`: answer
682
+ with `respond`, `edit` without interactive components or `replace`, and on a view
683
+ with durable fields `rerender()` raises `NoViewError`. `respond` continues what was
684
+ sent before the failure, as it would in the handler.
685
+ error
686
+ The exception.
687
+ stage
688
+ Where it was raised.
689
+ """
690
+
691
+ ctx: ComponentContext
692
+ error: Exception
693
+ stage: FailureStage