kroxy 1.0.2__tar.gz → 1.0.3__tar.gz

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 (32) hide show
  1. {kroxy-1.0.2/kroxy.egg-info → kroxy-1.0.3}/PKG-INFO +2 -2
  2. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/__init__.py +31 -49
  3. kroxy-1.0.3/kroxy/discord/__init__.py +31 -0
  4. kroxy-1.0.3/kroxy/discord/cache.py +202 -0
  5. kroxy-1.0.3/kroxy/discord/embed.py +275 -0
  6. kroxy-1.0.3/kroxy/discord/logger.py +237 -0
  7. kroxy-1.0.3/kroxy/discord/moderation.py +534 -0
  8. kroxy-1.0.3/kroxy/discord/webhook.py +238 -0
  9. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/website/__init__.py +3 -2
  10. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/website/fetch.py +93 -0
  11. kroxy-1.0.3/kroxy/website/monitor.py +222 -0
  12. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/website/scraper.py +168 -0
  13. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/website/utils.py +121 -0
  14. {kroxy-1.0.2 → kroxy-1.0.3/kroxy.egg-info}/PKG-INFO +2 -2
  15. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy.egg-info/SOURCES.txt +6 -0
  16. {kroxy-1.0.2 → kroxy-1.0.3}/pyproject.toml +2 -2
  17. kroxy-1.0.2/kroxy/discord/__init__.py +0 -22
  18. {kroxy-1.0.2 → kroxy-1.0.3}/LICENSE +0 -0
  19. {kroxy-1.0.2 → kroxy-1.0.3}/MANIFEST.in +0 -0
  20. {kroxy-1.0.2 → kroxy-1.0.3}/README.md +0 -0
  21. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/antinuke.py +0 -0
  22. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/api.py +0 -0
  23. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/checkers.py +0 -0
  24. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/commands.py +0 -0
  25. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/giveaway.py +0 -0
  26. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/music.py +0 -0
  27. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy/discord/utils.py +0 -0
  28. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy.egg-info/dependency_links.txt +0 -0
  29. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy.egg-info/requires.txt +0 -0
  30. {kroxy-1.0.2 → kroxy-1.0.3}/kroxy.egg-info/top_level.txt +0 -0
  31. {kroxy-1.0.2 → kroxy-1.0.3}/setup.cfg +0 -0
  32. {kroxy-1.0.2 → kroxy-1.0.3}/setup.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kroxy
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: A powerful Python toolkit for building automation, integrations, and bots.
5
5
  Author-email: kroxy <kroxy@example.com>
6
6
  License-Expression: LicenseRef-Proprietary
7
7
  Project-URL: Homepage, https://pypi.org/project/kroxy
8
- Keywords: bot,automation,antinuke,giveaway,music,scraper,toolkit,utility
8
+ Keywords: bot,automation,antinuke,giveaway,music,scraper,webhook,cache,moderation,toolkit
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
@@ -6,15 +6,10 @@ A powerful Python toolkit for building automation, integrations, and bots.
6
6
  All classes are available directly from the top-level `kroxy` namespace.
7
7
 
8
8
  import kroxy
9
- kroxy.AntiNuke(...)
10
- kroxy.GiveawayManager()
11
- kroxy.Fetcher()
12
- kroxy.Scraper.get_title(html)
13
- kroxy.WebUtils.slugify("Hello World")
14
-
15
- from kroxy import AntiNuke, Utils, Checkers, Fetcher, Scraper, WebUtils
16
- from kroxy import discord # discord submodule
17
- from kroxy import website # website submodule
9
+ from kroxy import AntiNuke, Embed, Moderation, Cache, Webhook
10
+ from kroxy import Fetcher, Scraper, WebUtils, Monitor
11
+ from kroxy import discord # full discord submodule
12
+ from kroxy import website # full website submodule
18
13
 
19
14
  License
20
15
  -------
@@ -22,7 +17,7 @@ Proprietary — Copyright (c) 2026 kroxy. All rights reserved.
22
17
  Unauthorized copying, modification, or redistribution is strictly prohibited.
23
18
  """
24
19
 
25
- __version__ = "1.0.2"
20
+ __version__ = "1.0.3"
26
21
  __author__ = "kroxy"
27
22
  __license__ = "Proprietary"
28
23
  __copyright__ = "Copyright (c) 2026 kroxy. All rights reserved."
@@ -33,70 +28,57 @@ from kroxy import website
33
28
 
34
29
  # ── Discord ────────────────────────────────────────────────────────────────────
35
30
  from kroxy.discord.api import DiscordAPI
36
-
37
- from kroxy.discord.commands import (
38
- SlashCommand,
39
- PrefixCommand,
40
- Option,
41
- CommandRegistry,
42
- CooldownError,
43
- )
44
-
31
+ from kroxy.discord.commands import SlashCommand, PrefixCommand, Option, CommandRegistry, CooldownError
45
32
  from kroxy.discord.utils import Utils
46
-
47
33
  from kroxy.discord.antinuke import AntiNuke, ActionLog
48
-
49
34
  from kroxy.discord.checkers import Checkers, CheckFailed
50
-
51
35
  from kroxy.discord.giveaway import GiveawayManager, Giveaway, GiveawayEntry
52
-
53
- from kroxy.discord.music import (
54
- MusicPlayerManager,
55
- MusicPlayer,
56
- MusicQueue,
57
- Track,
58
- LoopMode,
59
- )
36
+ from kroxy.discord.music import MusicPlayerManager, MusicPlayer, MusicQueue, Track, LoopMode
37
+ from kroxy.discord.moderation import Moderation, Warning
38
+ from kroxy.discord.embed import Embed
39
+ from kroxy.discord.logger import ModLogger
40
+ from kroxy.discord.cache import Cache
41
+ from kroxy.discord.webhook import Webhook
60
42
 
61
43
  # ── Website ────────────────────────────────────────────────────────────────────
62
44
  from kroxy.website.fetch import Fetcher
63
45
  from kroxy.website.scraper import Scraper
64
46
  from kroxy.website.utils import WebUtils
47
+ from kroxy.website.monitor import Monitor, CheckResult
65
48
 
66
49
  __all__ = [
67
50
  # Submodules
68
- "discord",
69
- "website",
51
+ "discord", "website",
70
52
  # Discord — API
71
53
  "DiscordAPI",
72
54
  # Discord — Commands
73
- "SlashCommand",
74
- "PrefixCommand",
75
- "Option",
76
- "CommandRegistry",
77
- "CooldownError",
55
+ "SlashCommand", "PrefixCommand", "Option", "CommandRegistry", "CooldownError",
78
56
  # Discord — Utils
79
57
  "Utils",
80
58
  # Discord — Anti-Nuke
81
- "AntiNuke",
82
- "ActionLog",
59
+ "AntiNuke", "ActionLog",
83
60
  # Discord — Checkers
84
- "Checkers",
85
- "CheckFailed",
61
+ "Checkers", "CheckFailed",
86
62
  # Discord — Giveaway
87
- "GiveawayManager",
88
- "Giveaway",
89
- "GiveawayEntry",
63
+ "GiveawayManager", "Giveaway", "GiveawayEntry",
90
64
  # Discord — Music
91
- "MusicPlayerManager",
92
- "MusicPlayer",
93
- "MusicQueue",
94
- "Track",
95
- "LoopMode",
65
+ "MusicPlayerManager", "MusicPlayer", "MusicQueue", "Track", "LoopMode",
66
+ # Discord — Moderation
67
+ "Moderation", "Warning",
68
+ # Discord — Embed
69
+ "Embed",
70
+ # Discord — Logger
71
+ "ModLogger",
72
+ # Discord — Cache
73
+ "Cache",
74
+ # Discord — Webhook
75
+ "Webhook",
96
76
  # Website — Fetch
97
77
  "Fetcher",
98
78
  # Website — Scraper
99
79
  "Scraper",
100
80
  # Website — Utils
101
81
  "WebUtils",
82
+ # Website — Monitor
83
+ "Monitor", "CheckResult",
102
84
  ]
@@ -0,0 +1,31 @@
1
+ """
2
+ kroxy.discord - Discord bot utilities, automation, and server management.
3
+ """
4
+
5
+ from kroxy.discord.api import DiscordAPI
6
+ from kroxy.discord.commands import SlashCommand, PrefixCommand, Option, CommandRegistry, CooldownError
7
+ from kroxy.discord.utils import Utils
8
+ from kroxy.discord.antinuke import AntiNuke, ActionLog
9
+ from kroxy.discord.checkers import Checkers, CheckFailed
10
+ from kroxy.discord.giveaway import GiveawayManager, Giveaway, GiveawayEntry
11
+ from kroxy.discord.music import MusicPlayerManager, MusicPlayer, MusicQueue, Track, LoopMode
12
+ from kroxy.discord.moderation import Moderation, Warning
13
+ from kroxy.discord.embed import Embed
14
+ from kroxy.discord.logger import ModLogger
15
+ from kroxy.discord.cache import Cache
16
+ from kroxy.discord.webhook import Webhook
17
+
18
+ __all__ = [
19
+ "DiscordAPI",
20
+ "SlashCommand", "PrefixCommand", "Option", "CommandRegistry", "CooldownError",
21
+ "Utils",
22
+ "AntiNuke", "ActionLog",
23
+ "Checkers", "CheckFailed",
24
+ "GiveawayManager", "Giveaway", "GiveawayEntry",
25
+ "MusicPlayerManager", "MusicPlayer", "MusicQueue", "Track", "LoopMode",
26
+ "Moderation", "Warning",
27
+ "Embed",
28
+ "ModLogger",
29
+ "Cache",
30
+ "Webhook",
31
+ ]
@@ -0,0 +1,202 @@
1
+ """
2
+ kroxy.discord.cache - TTL in-memory cache for bot data.
3
+ """
4
+
5
+ import time
6
+ import asyncio
7
+ from typing import Any, Callable, Dict, List, Optional, Tuple
8
+
9
+
10
+ class _Entry:
11
+ __slots__ = ("value", "expires_at")
12
+
13
+ def __init__(self, value: Any, expires_at: float):
14
+ self.value = value
15
+ self.expires_at = expires_at
16
+
17
+ def is_expired(self) -> bool:
18
+ return time.time() > self.expires_at
19
+
20
+
21
+ class Cache:
22
+ """
23
+ Thread-safe TTL in-memory cache.
24
+
25
+ Ideal for caching Discord API responses, guild configs,
26
+ user data, cooldowns, and any frequently-read values.
27
+
28
+ Usage:
29
+ from kroxy import Cache
30
+
31
+ cache = Cache(default_ttl=300) # 5 minute default TTL
32
+ cache.set("user:123", user_data)
33
+ user = cache.get("user:123")
34
+ cache.delete("user:123")
35
+
36
+ # get or compute
37
+ guild = await cache.get_or_set("guild:456", lambda: fetch_guild(456), ttl=60)
38
+ """
39
+
40
+ def __init__(self, default_ttl: int = 300):
41
+ """
42
+ Args:
43
+ default_ttl: Default time-to-live in seconds for cached entries.
44
+ """
45
+ self._store: Dict[str, _Entry] = {}
46
+ self.default_ttl = default_ttl
47
+
48
+ def set(self, key: str, value: Any, ttl: int = None) -> None:
49
+ """
50
+ Store a value in the cache.
51
+
52
+ Args:
53
+ key: Cache key string.
54
+ value: Value to store (any type).
55
+ ttl: Time-to-live in seconds. Uses default_ttl if not specified.
56
+ """
57
+ ttl = ttl if ttl is not None else self.default_ttl
58
+ self._store[key] = _Entry(value=value, expires_at=time.time() + ttl)
59
+
60
+ def get(self, key: str, default: Any = None) -> Any:
61
+ """
62
+ Retrieve a value from the cache.
63
+
64
+ Args:
65
+ key: Cache key string.
66
+ default: Value to return if key is missing or expired.
67
+
68
+ Returns:
69
+ Cached value or default.
70
+ """
71
+ entry = self._store.get(key)
72
+ if entry is None or entry.is_expired():
73
+ self._store.pop(key, None)
74
+ return default
75
+ return entry.value
76
+
77
+ def delete(self, key: str) -> bool:
78
+ """
79
+ Remove a key from the cache.
80
+
81
+ Returns:
82
+ True if the key existed and was removed, False otherwise.
83
+ """
84
+ return self._store.pop(key, None) is not None
85
+
86
+ def has(self, key: str) -> bool:
87
+ """
88
+ Check if a key exists in the cache and has not expired.
89
+
90
+ Returns:
91
+ True if the key is present and valid.
92
+ """
93
+ entry = self._store.get(key)
94
+ if entry is None or entry.is_expired():
95
+ self._store.pop(key, None)
96
+ return False
97
+ return True
98
+
99
+ def clear(self) -> int:
100
+ """
101
+ Remove all entries from the cache.
102
+
103
+ Returns:
104
+ Number of entries that were cleared.
105
+ """
106
+ count = len(self._store)
107
+ self._store.clear()
108
+ return count
109
+
110
+ def size(self) -> int:
111
+ """
112
+ Count non-expired entries.
113
+
114
+ Returns:
115
+ Number of valid (non-expired) entries.
116
+ """
117
+ now = time.time()
118
+ return sum(1 for e in self._store.values() if e.expires_at > now)
119
+
120
+ def keys(self) -> List[str]:
121
+ """
122
+ Return all active (non-expired) keys.
123
+
124
+ Returns:
125
+ List of key strings.
126
+ """
127
+ now = time.time()
128
+ return [k for k, e in self._store.items() if e.expires_at > now]
129
+
130
+ async def get_or_set(self, key: str, factory: Callable,
131
+ ttl: int = None) -> Any:
132
+ """
133
+ Return cached value if available, otherwise call factory(), cache and return the result.
134
+
135
+ Args:
136
+ key: Cache key.
137
+ factory: Async or sync callable that produces the value.
138
+ ttl: TTL for the new entry.
139
+
140
+ Returns:
141
+ Cached or freshly computed value.
142
+ """
143
+ value = self.get(key)
144
+ if value is not None:
145
+ return value
146
+ if asyncio.iscoroutinefunction(factory):
147
+ value = await factory()
148
+ else:
149
+ value = factory()
150
+ self.set(key, value, ttl=ttl)
151
+ return value
152
+
153
+ def expire(self, key: str, new_ttl: int) -> bool:
154
+ """
155
+ Update the TTL of an existing non-expired cache entry.
156
+
157
+ Args:
158
+ key: Cache key.
159
+ new_ttl: New TTL in seconds from now.
160
+
161
+ Returns:
162
+ True if the key existed and was updated, False otherwise.
163
+ """
164
+ entry = self._store.get(key)
165
+ if entry is None or entry.is_expired():
166
+ return False
167
+ entry.expires_at = time.time() + new_ttl
168
+ return True
169
+
170
+ def cleanup(self) -> int:
171
+ """
172
+ Remove all expired entries to free memory.
173
+
174
+ Returns:
175
+ Number of expired entries removed.
176
+ """
177
+ now = time.time()
178
+ expired = [k for k, e in self._store.items() if e.expires_at <= now]
179
+ for k in expired:
180
+ del self._store[k]
181
+ return len(expired)
182
+
183
+ def get_ttl(self, key: str) -> Optional[float]:
184
+ """
185
+ Get remaining TTL for a key in seconds.
186
+
187
+ Returns:
188
+ Seconds remaining, or None if key is missing/expired.
189
+ """
190
+ entry = self._store.get(key)
191
+ if entry is None or entry.is_expired():
192
+ return None
193
+ return max(0.0, entry.expires_at - time.time())
194
+
195
+ def __len__(self) -> int:
196
+ return self.size()
197
+
198
+ def __contains__(self, key: str) -> bool:
199
+ return self.has(key)
200
+
201
+ def __repr__(self) -> str:
202
+ return f"<Cache size={self.size()} default_ttl={self.default_ttl}s>"
@@ -0,0 +1,275 @@
1
+ """
2
+ kroxy.discord.embed - OOP chainable Discord embed builder.
3
+ """
4
+
5
+ import datetime
6
+ import json
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class Embed:
11
+ """
12
+ Chainable, OOP-style Discord embed builder.
13
+
14
+ All setter methods return `self` so they can be chained.
15
+
16
+ Discord limits enforced by is_valid() / truncate_all():
17
+ - Title: 256 chars
18
+ - Description: 4096 chars
19
+ - Fields: 25 max
20
+ - Field name: 256 chars
21
+ - Field value: 1024 chars
22
+ - Footer text: 2048 chars
23
+ - Author name: 256 chars
24
+ - Total chars: 6000
25
+
26
+ Usage:
27
+ embed = (
28
+ Embed(title="Hello", color=0x5865F2)
29
+ .set_description("This is an embed.")
30
+ .add_field("Field 1", "Value 1", inline=True)
31
+ .add_field("Field 2", "Value 2", inline=True)
32
+ .set_footer("kroxy")
33
+ .set_timestamp()
34
+ )
35
+ await api.send_message(channel_id, embed=embed.to_dict())
36
+ """
37
+
38
+ LIMITS = {
39
+ "title": 256,
40
+ "description": 4096,
41
+ "field_name": 256,
42
+ "field_value": 1024,
43
+ "footer_text": 2048,
44
+ "author_name": 256,
45
+ "total": 6000,
46
+ "fields": 25,
47
+ }
48
+
49
+ def __init__(self, title: str = None, description: str = None,
50
+ color: int = 0x5865F2):
51
+ self._title: Optional[str] = title
52
+ self._description: Optional[str] = description
53
+ self._color: int = color
54
+ self._fields: List[Dict] = []
55
+ self._footer: Optional[Dict] = None
56
+ self._thumbnail: Optional[Dict] = None
57
+ self._image: Optional[Dict] = None
58
+ self._author: Optional[Dict] = None
59
+ self._timestamp: Optional[str] = None
60
+ self._url: Optional[str] = None
61
+
62
+ # ── Setters (all chainable) ───────────────────────────────────────────────
63
+
64
+ def set_title(self, title: str) -> "Embed":
65
+ """Set the embed title."""
66
+ self._title = title
67
+ return self
68
+
69
+ def set_description(self, description: str) -> "Embed":
70
+ """Set the embed description."""
71
+ self._description = description
72
+ return self
73
+
74
+ def set_color(self, color: int) -> "Embed":
75
+ """Set the embed color as an integer (e.g. 0x5865F2)."""
76
+ self._color = color
77
+ return self
78
+
79
+ def set_url(self, url: str) -> "Embed":
80
+ """Set the embed URL (makes the title a hyperlink)."""
81
+ self._url = url
82
+ return self
83
+
84
+ def add_field(self, name: str, value: str,
85
+ inline: bool = False) -> "Embed":
86
+ """Add a field to the embed."""
87
+ self._fields.append({"name": name, "value": value, "inline": inline})
88
+ return self
89
+
90
+ def remove_field(self, index: int) -> "Embed":
91
+ """Remove a field by its index (0-based)."""
92
+ if 0 <= index < len(self._fields):
93
+ self._fields.pop(index)
94
+ return self
95
+
96
+ def clear_fields(self) -> "Embed":
97
+ """Remove all fields from the embed."""
98
+ self._fields.clear()
99
+ return self
100
+
101
+ def set_footer(self, text: str, icon_url: str = None) -> "Embed":
102
+ """Set the embed footer."""
103
+ self._footer = {"text": text}
104
+ if icon_url:
105
+ self._footer["icon_url"] = icon_url
106
+ return self
107
+
108
+ def set_thumbnail(self, url: str) -> "Embed":
109
+ """Set the embed thumbnail (small image in top-right corner)."""
110
+ self._thumbnail = {"url": url}
111
+ return self
112
+
113
+ def set_image(self, url: str) -> "Embed":
114
+ """Set the embed large image (shown below fields)."""
115
+ self._image = {"url": url}
116
+ return self
117
+
118
+ def set_author(self, name: str, url: str = None,
119
+ icon_url: str = None) -> "Embed":
120
+ """Set the embed author section."""
121
+ self._author = {"name": name}
122
+ if url:
123
+ self._author["url"] = url
124
+ if icon_url:
125
+ self._author["icon_url"] = icon_url
126
+ return self
127
+
128
+ def set_timestamp(self, dt: datetime.datetime = None) -> "Embed":
129
+ """
130
+ Set the embed timestamp.
131
+ If dt is None, uses the current UTC time.
132
+ """
133
+ dt = dt or datetime.datetime.utcnow()
134
+ self._timestamp = dt.isoformat()
135
+ return self
136
+
137
+ # ── Export ────────────────────────────────────────────────────────────────
138
+
139
+ def to_dict(self) -> Dict:
140
+ """Export the embed as a Discord-compatible dict."""
141
+ data: Dict[str, Any] = {"color": self._color}
142
+ if self._title:
143
+ data["title"] = self._title
144
+ if self._description:
145
+ data["description"] = self._description
146
+ if self._url:
147
+ data["url"] = self._url
148
+ if self._fields:
149
+ data["fields"] = list(self._fields)
150
+ if self._footer:
151
+ data["footer"] = self._footer
152
+ if self._thumbnail:
153
+ data["thumbnail"] = self._thumbnail
154
+ if self._image:
155
+ data["image"] = self._image
156
+ if self._author:
157
+ data["author"] = self._author
158
+ if self._timestamp:
159
+ data["timestamp"] = self._timestamp
160
+ return data
161
+
162
+ @classmethod
163
+ def from_dict(cls, data: Dict) -> "Embed":
164
+ """
165
+ Create an Embed from an existing Discord embed dict.
166
+
167
+ Returns:
168
+ Embed instance populated from the dict.
169
+ """
170
+ e = cls(
171
+ title=data.get("title"),
172
+ description=data.get("description"),
173
+ color=data.get("color", 0x5865F2),
174
+ )
175
+ e._url = data.get("url")
176
+ e._fields = list(data.get("fields", []))
177
+ e._footer = data.get("footer")
178
+ e._thumbnail = data.get("thumbnail")
179
+ e._image = data.get("image")
180
+ e._author = data.get("author")
181
+ e._timestamp = data.get("timestamp")
182
+ return e
183
+
184
+ def copy(self) -> "Embed":
185
+ """Return a deep copy of this embed."""
186
+ return Embed.from_dict(self.to_dict())
187
+
188
+ def merge(self, other: "Embed") -> "Embed":
189
+ """
190
+ Merge another embed's fields into this one (fields are appended).
191
+
192
+ Returns:
193
+ self (chainable)
194
+ """
195
+ for f in other._fields:
196
+ self._fields.append(dict(f))
197
+ if not self._description and other._description:
198
+ self._description = other._description
199
+ if not self._footer and other._footer:
200
+ self._footer = dict(other._footer)
201
+ return self
202
+
203
+ # ── Validation ────────────────────────────────────────────────────────────
204
+
205
+ @property
206
+ def total_chars(self) -> int:
207
+ """Total character count across all text fields."""
208
+ total = 0
209
+ if self._title:
210
+ total += len(self._title)
211
+ if self._description:
212
+ total += len(self._description)
213
+ for f in self._fields:
214
+ total += len(f.get("name", "")) + len(f.get("value", ""))
215
+ if self._footer:
216
+ total += len(self._footer.get("text", ""))
217
+ if self._author:
218
+ total += len(self._author.get("name", ""))
219
+ return total
220
+
221
+ def is_valid(self) -> bool:
222
+ """
223
+ Check that the embed is within Discord's character and count limits.
224
+
225
+ Returns:
226
+ True if valid, False otherwise.
227
+ """
228
+ if self._title and len(self._title) > self.LIMITS["title"]:
229
+ return False
230
+ if self._description and len(self._description) > self.LIMITS["description"]:
231
+ return False
232
+ if len(self._fields) > self.LIMITS["fields"]:
233
+ return False
234
+ for f in self._fields:
235
+ if len(f.get("name", "")) > self.LIMITS["field_name"]:
236
+ return False
237
+ if len(f.get("value", "")) > self.LIMITS["field_value"]:
238
+ return False
239
+ if self._footer and len(self._footer.get("text", "")) > self.LIMITS["footer_text"]:
240
+ return False
241
+ if self._author and len(self._author.get("name", "")) > self.LIMITS["author_name"]:
242
+ return False
243
+ if self.total_chars > self.LIMITS["total"]:
244
+ return False
245
+ return True
246
+
247
+ def truncate_all(self) -> "Embed":
248
+ """
249
+ Auto-truncate all text fields to Discord's limits.
250
+
251
+ Returns:
252
+ self (chainable)
253
+ """
254
+ def _cut(text: str, limit: int) -> str:
255
+ return text[: limit - 3] + "..." if len(text) > limit else text
256
+
257
+ if self._title:
258
+ self._title = _cut(self._title, self.LIMITS["title"])
259
+ if self._description:
260
+ self._description = _cut(self._description, self.LIMITS["description"])
261
+ if self._footer and self._footer.get("text"):
262
+ self._footer["text"] = _cut(self._footer["text"], self.LIMITS["footer_text"])
263
+ if self._author and self._author.get("name"):
264
+ self._author["name"] = _cut(self._author["name"], self.LIMITS["author_name"])
265
+ self._fields = self._fields[: self.LIMITS["fields"]]
266
+ for f in self._fields:
267
+ f["name"] = _cut(f.get("name", ""), self.LIMITS["field_name"])
268
+ f["value"] = _cut(f.get("value", ""), self.LIMITS["field_value"])
269
+ return self
270
+
271
+ def __repr__(self) -> str:
272
+ return (
273
+ f"<Embed title={self._title!r} fields={len(self._fields)} "
274
+ f"chars={self.total_chars} valid={self.is_valid()}>"
275
+ )