feat: persist views (#120)
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled

This commit is contained in:
Slipstream 2025-06-15 20:39:12 -06:00 committed by GitHub
parent 80f64c1f73
commit aa55aa1d4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 17 deletions

View File

@ -2,8 +2,11 @@
The main Client class for interacting with the Discord API.
"""
import asyncio
import signal
import asyncio
import signal
import json
import os
import importlib
from typing import (
Optional,
Callable,
@ -16,7 +19,9 @@ from typing import (
Dict,
cast,
)
from types import ModuleType
from types import ModuleType
PERSISTENT_VIEWS_FILE = "persistent_views.json"
from datetime import datetime, timedelta
@ -77,7 +82,7 @@ def _update_list(lst: List[Any], item: Any) -> None:
lst.append(item)
class Client:
class Client:
"""
Represents a client connection that connects to Discord.
This class is used to interact with the Discord WebSocket and API.
@ -193,7 +198,10 @@ class Client:
self._views: Dict[Snowflake, "View"] = {}
self._persistent_views: Dict[str, "View"] = {}
self._voice_clients: Dict[Snowflake, VoiceClient] = {}
self._webhooks: Dict[Snowflake, "Webhook"] = {}
self._webhooks: Dict[Snowflake, "Webhook"] = {}
# Load persistent views stored on disk
self._load_persistent_views()
# Default whether replies mention the user
self.mention_replies: bool = mention_replies
@ -210,13 +218,46 @@ class Client:
self.loop.add_signal_handler(
signal.SIGTERM, lambda: self.loop.create_task(self.close())
)
except NotImplementedError:
# add_signal_handler is not available on all platforms (e.g., Windows default event loop policy)
# Users on these platforms would need to handle shutdown differently.
print(
"Warning: Signal handlers for SIGINT/SIGTERM could not be added. "
"Graceful shutdown via signals might not work as expected on this platform."
)
except NotImplementedError:
# add_signal_handler is not available on all platforms (e.g., Windows default event loop policy)
# Users on these platforms would need to handle shutdown differently.
print(
"Warning: Signal handlers for SIGINT/SIGTERM could not be added. "
"Graceful shutdown via signals might not work as expected on this platform."
)
def _load_persistent_views(self) -> None:
"""Load registered persistent views from disk."""
if not os.path.isfile(PERSISTENT_VIEWS_FILE):
return
try:
with open(PERSISTENT_VIEWS_FILE, "r") as fp:
mapping = json.load(fp)
except Exception as e: # pragma: no cover - best effort load
print(f"Failed to load persistent views: {e}")
return
for custom_id, path in mapping.items():
try:
module_name, class_name = path.rsplit(".", 1)
module = importlib.import_module(module_name)
cls = getattr(module, class_name)
view = cls()
self._persistent_views[custom_id] = view
except Exception as e: # pragma: no cover - best effort load
print(f"Failed to initialize persistent view {path}: {e}")
def _save_persistent_views(self) -> None:
"""Persist registered views to disk."""
data = {}
for custom_id, view in self._persistent_views.items():
cls = view.__class__
data[custom_id] = f"{cls.__module__}.{cls.__name__}"
try:
with open(PERSISTENT_VIEWS_FILE, "w") as fp:
json.dump(data, fp)
except Exception as e: # pragma: no cover - best effort save
print(f"Failed to save persistent views: {e}")
async def _initialize_gateway(self):
"""Initializes the GatewayClient if it doesn't exist."""
@ -1707,11 +1748,13 @@ class Client:
for item in view.children:
if item.custom_id: # Ensure custom_id is not None
if item.custom_id in self._persistent_views:
raise ValueError(
f"A component with custom_id '{item.custom_id}' is already registered."
)
self._persistent_views[item.custom_id] = view
if item.custom_id in self._persistent_views:
raise ValueError(
f"A component with custom_id '{item.custom_id}' is already registered."
)
self._persistent_views[item.custom_id] = view
self._save_persistent_views()
# --- Application Command Methods ---
async def process_interaction(self, interaction: Interaction) -> None:

View File

@ -157,6 +157,22 @@ container = Container(
A container can itself contain layout and content components, letting you build complex messages.
## Persistent Views
Views with ``timeout=None`` are persistent. Their ``custom_id`` components are saved to ``persistent_views.json`` so they survive bot restarts.
```python
class MyView(View):
@button(label="Press", custom_id="press")
async def handle(self, view, inter):
await inter.respond("Pressed!")
client.add_persistent_view(MyView())
```
When the client starts, it loads this file and registers each view again. Remove
the file to clear stored views.
## Next Steps
- [Slash Commands](slash_commands.md)