Repository: Pycord-Development/pycord

Issue:
Paginator error upon failed interaction
### Summary

If a paginator update fails inside callback, the paginator can enter an un-recoverable state

### Reproduction Steps

1. Create a paginator with 2 pages
2. Click the right arrow/next page button
   - The interaction must fail, you can trigger this by adding a debug breakpoint or a sleep function inside `Paginator.goto_page`, or by throttling your network connection
3. Observe the error inside the console: `discord.errors.NotFound: 404 Not Found (error code: 10062): Unknown interaction`
4. Press the same button again
5. Observe this error in your console:
```
    page = self.pages[page_number]
           ~~~~~~~~~~^^^^^^^^^^^^^
IndexError: list index out of range
```

### Minimal Reproducible Code

```python
import os

from discord import Bot
from discord.ext.pages import Paginator
from discord.flags import Intents

bot = Bot(
    intents=Intents.default(),
)


@bot.command()
async def test(ctx):
    paginator = Paginator(
        pages=["page 1", "page 2"],
    )
    await paginator.respond(ctx.interaction)


bot.run(os.getenv("DISCORD_TOKEN"))
```


### Expected Results

The paginator should fallback to its previous state in the event of a failure, so the second update should be a retry of the first.

### Actual Results

There is a silent failure, `current_page` is updated but the paginator is not. 

### Intents

Intents.default()

### System Information

- Python v3.12.0-final
- py-cord v2.5.1-final
- aiohttp v3.9.5
- system info: Darwin 23.4.0 Darwin Kernel Version 23.4.0: Wed Feb 21 21:44:06 PST 2024; root:xnu-10063.101.15~2/RELEASE_ARM64_T8103


### Checklist

- [X] I have searched the open issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have removed my token from display, if visible.

### Additional Context

_No response_


Discussion:

Resolving patch:
diff --git a/discord/ext/pages/pagination.py b/discord/ext/pages/pagination.py
--- a/discord/ext/pages/pagination.py
+++ b/discord/ext/pages/pagination.py
@@ -27,8 +27,10 @@
 from typing import List
 
 import discord
+from discord.errors import DiscordException
 from discord.ext.bridge import BridgeContext
 from discord.ext.commands import Context
+from discord.file import File
 from discord.member import Member
 from discord.user import User
 
@@ -103,26 +105,25 @@ async def callback(self, interaction: discord.Interaction):
         interaction: :class:`discord.Interaction`
             The interaction created by clicking the navigation button.
         """
+        new_page = self.paginator.current_page
         if self.button_type == "first":
-            self.paginator.current_page = 0
+            new_page = 0
         elif self.button_type == "prev":
             if self.paginator.loop_pages and self.paginator.current_page == 0:
-                self.paginator.current_page = self.paginator.page_count
+                new_page = self.paginator.page_count
             else:
-                self.paginator.current_page -= 1
+                new_page -= 1
         elif self.button_type == "next":
             if (
                 self.paginator.loop_pages
                 and self.paginator.current_page == self.paginator.page_count
             ):
-                self.paginator.current_page = 0
+                new_page = 0
             else:
-                self.paginator.current_page += 1
+                new_page += 1
         elif self.button_type == "last":
-            self.paginator.current_page = self.paginator.page_count
-        await self.paginator.goto_page(
-            page_number=self.paginator.current_page, interaction=interaction
-        )
+            new_page = self.paginator.page_count
+        await self.paginator.goto_page(page_number=new_page, interaction=interaction)
 
 
 class Page:
@@ -656,6 +657,20 @@ async def cancel(
         else:
             await self.message.edit(view=self)
 
+    def _goto_page(self, page_number: int = 0) -> tuple[Page, list[File] | None]:
+        self.current_page = page_number
+        self.update_buttons()
+
+        page = self.pages[page_number]
+        page = self.get_page_content(page)
+
+        if page.custom_view:
+            self.update_custom_view(page.custom_view)
+
+        files = page.update_files()
+
+        return page, files
+
     async def goto_page(
         self, page_number: int = 0, *, interaction: discord.Interaction | None = None
     ) -> None:
@@ -680,42 +695,34 @@ async def goto_page(
         :class:`~discord.Message`
             The message associated with the paginator.
         """
-        self.update_buttons()
-        self.current_page = page_number
-        if self.show_indicator:
-            try:
-                self.buttons["page_indicator"][
-                    "object"
-                ].label = f"{self.current_page + 1}/{self.page_count + 1}"
-            except KeyError:
-                pass
-
-        page = self.pages[page_number]
-        page = self.get_page_content(page)
+        old_page = self.current_page
+        page, files = self._goto_page(page_number)
 
-        if page.custom_view:
-            self.update_custom_view(page.custom_view)
-
-        files = page.update_files()
+        try:
+            if interaction:
+                await interaction.response.defer()  # needed to force webhook message edit route for files kwarg support
+                await interaction.followup.edit_message(
+                    message_id=self.message.id,
+                    content=page.content,
+                    embeds=page.embeds,
+                    attachments=[],
+                    files=files or [],
+                    view=self,
+                )
+            else:
+                await self.message.edit(
+                    content=page.content,
+                    embeds=page.embeds,
+                    attachments=[],
+                    files=files or [],
+                    view=self,
+                )
+        except DiscordException:
+            # Something went wrong, and the paginator couldn't be updated.
+            # Revert our changes and propagate the error.
+            self._goto_page(old_page)
+            raise
 
-        if interaction:
-            await interaction.response.defer()  # needed to force webhook message edit route for files kwarg support
-            await interaction.followup.edit_message(
-                message_id=self.message.id,
-                content=page.content,
-                embeds=page.embeds,
-                attachments=[],
-                files=files or [],
-                view=self,
-            )
-        else:
-            await self.message.edit(
-                content=page.content,
-                embeds=page.embeds,
-                attachments=[],
-                files=files or [],
-                view=self,
-            )
         if self.trigger_on_display:
             await self.page_action(interaction=interaction)
 


Test patch: