feat: persist views (#120)
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
This commit is contained in:
parent
80f64c1f73
commit
aa55aa1d4c
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user