ezytools 0.1.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.
- ezytools/_init_.py +3 -0
- ezytools/core.py +629 -0
- ezytools-0.1.0.dist-info/METADATA +245 -0
- ezytools-0.1.0.dist-info/RECORD +6 -0
- ezytools-0.1.0.dist-info/WHEEL +4 -0
- ezytools-0.1.0.dist-info/licenses/LICENSE +47 -0
ezytools/_init_.py
ADDED
ezytools/core.py
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import random
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
import time
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
# ==================================================
|
|
11
|
+
# EZYBOT UTILITIES
|
|
12
|
+
# ==================================================
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format="[ezybot] %(levelname)s: %(message)s"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("ezybot")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ==================================================
|
|
23
|
+
# CONFIGURATION
|
|
24
|
+
# ==================================================
|
|
25
|
+
|
|
26
|
+
def get_owner_id():
|
|
27
|
+
"""Get the bot owner's user ID from an environment variable."""
|
|
28
|
+
return os.environ.get("EZYBOT_OWNER_ID", "")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_owner(user_id):
|
|
32
|
+
"""Check whether a user is the configured bot owner."""
|
|
33
|
+
owner_id = get_owner_id()
|
|
34
|
+
return bool(owner_id) and str(user_id) == str(owner_id)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ==================================================
|
|
38
|
+
# LOGGING
|
|
39
|
+
# ==================================================
|
|
40
|
+
|
|
41
|
+
def log(message):
|
|
42
|
+
logger.info(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def log_warning(message):
|
|
46
|
+
logger.warning(message)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def log_error(message):
|
|
50
|
+
logger.error(message)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def log_moderation(action, moderator, target, reason="No reason provided"):
|
|
54
|
+
log(
|
|
55
|
+
f"MODERATION | {action} | "
|
|
56
|
+
f"Moderator: {moderator} | "
|
|
57
|
+
f"Target: {target} | "
|
|
58
|
+
f"Reason: {reason}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ==================================================
|
|
63
|
+
# MODERATION
|
|
64
|
+
# ==================================================
|
|
65
|
+
|
|
66
|
+
# Stored warnings, separated by server and user.
|
|
67
|
+
# Note: these are stored in memory and reset when the bot restarts.
|
|
68
|
+
_warnings = {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _args(msg):
|
|
72
|
+
"""Get command arguments, respecting quoted text when possible."""
|
|
73
|
+
raw = getattr(msg, "args", "") or ""
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
return shlex.split(raw) if isinstance(raw, str) else list(raw)
|
|
77
|
+
except ValueError:
|
|
78
|
+
return raw.split()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _reply(msg, text):
|
|
82
|
+
msg.reply(text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _server_only(msg):
|
|
86
|
+
"""Ensure the command was used inside a server."""
|
|
87
|
+
if not getattr(msg, "server_id", None):
|
|
88
|
+
_reply(msg, "❌ This command only works in a server.")
|
|
89
|
+
return False
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _duration(value):
|
|
94
|
+
"""Convert 10m, 2h, or 1d to minutes, with a 28-day maximum."""
|
|
95
|
+
match = re.fullmatch(r"(\d+)([mhd]?)", str(value).lower())
|
|
96
|
+
|
|
97
|
+
if not match:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
amount = int(match.group(1))
|
|
101
|
+
unit = match.group(2) or "m"
|
|
102
|
+
minutes = amount * {"m": 1, "h": 60, "d": 1440}[unit]
|
|
103
|
+
|
|
104
|
+
if not 1 <= minutes <= 40320:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
return minutes
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run(msg, action, success):
|
|
111
|
+
"""Run a moderation action and provide a readable response."""
|
|
112
|
+
try:
|
|
113
|
+
action()
|
|
114
|
+
_reply(msg, success)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
status = getattr(exc, "status", None)
|
|
117
|
+
|
|
118
|
+
if status == 403:
|
|
119
|
+
_reply(msg, "❌ I don't have permission to do that.")
|
|
120
|
+
elif status == 404:
|
|
121
|
+
_reply(msg, "❌ Member or ban not found.")
|
|
122
|
+
else:
|
|
123
|
+
log_error(f"Moderation action failed: {exc}")
|
|
124
|
+
_reply(msg, f"❌ Action failed: {exc}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def kick(bot, msg):
|
|
128
|
+
"""Kick a user. Usage: /kick <user_id>."""
|
|
129
|
+
args = _args(msg)
|
|
130
|
+
|
|
131
|
+
if not args:
|
|
132
|
+
return _reply(msg, "Usage: /kick <user_id>")
|
|
133
|
+
if not _server_only(msg):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
user_id = get_target(bot, msg)
|
|
137
|
+
|
|
138
|
+
if not user_id:
|
|
139
|
+
return _reply(
|
|
140
|
+
msg,
|
|
141
|
+
"❌ User not found. Use a mention, username, or ID."
|
|
142
|
+
)
|
|
143
|
+
_run(
|
|
144
|
+
msg,
|
|
145
|
+
lambda: bot.kick(msg.server_id, user_id),
|
|
146
|
+
f"✅ Kicked `{user_id}`."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def ban(bot, msg):
|
|
151
|
+
"""Ban a user. Usage: /ban <user_id>."""
|
|
152
|
+
args = _args(msg)
|
|
153
|
+
|
|
154
|
+
if not args:
|
|
155
|
+
return _reply(msg, "Usage: /ban <user_id>")
|
|
156
|
+
if not _server_only(msg):
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
user_id = get_target(bot, msg)
|
|
160
|
+
|
|
161
|
+
if not user_id:
|
|
162
|
+
return _reply(
|
|
163
|
+
msg,
|
|
164
|
+
"❌ User not found. Use a mention, username, or ID."
|
|
165
|
+
)
|
|
166
|
+
_run(
|
|
167
|
+
msg,
|
|
168
|
+
lambda: bot.ban(msg.server_id, user_id),
|
|
169
|
+
f"🔨 Banned `{user_id}`."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def unban(bot, msg):
|
|
174
|
+
"""Unban a user. Usage: /unban <user_id>."""
|
|
175
|
+
args = _args(msg)
|
|
176
|
+
|
|
177
|
+
if not args:
|
|
178
|
+
return _reply(msg, "Usage: /unban <user_id>")
|
|
179
|
+
if not _server_only(msg):
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
user_id = args[0]
|
|
183
|
+
_run(
|
|
184
|
+
msg,
|
|
185
|
+
lambda: bot.unban(msg.server_id, user_id),
|
|
186
|
+
f"✅ Unbanned `{user_id}`."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def mute(bot, msg):
|
|
191
|
+
"""Timeout a user. Usage: /mute <user_id> <duration> [reason]."""
|
|
192
|
+
args = _args(msg)
|
|
193
|
+
|
|
194
|
+
if len(args) < 2:
|
|
195
|
+
return _reply(
|
|
196
|
+
msg,
|
|
197
|
+
"Usage: /mute <user_id> <duration> [reason]\n"
|
|
198
|
+
"Example: /mute 12345 30m spamming"
|
|
199
|
+
)
|
|
200
|
+
if not _server_only(msg):
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
user_id, duration = args[0], args[1]
|
|
204
|
+
minutes = _duration(duration)
|
|
205
|
+
|
|
206
|
+
if minutes is None:
|
|
207
|
+
return _reply(
|
|
208
|
+
msg,
|
|
209
|
+
"❌ Invalid duration. Use 10m, 2h, 1d, or a number of minutes "
|
|
210
|
+
"(maximum 28 days)."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
reason = " ".join(args[2:]) or "No reason provided"
|
|
214
|
+
|
|
215
|
+
_run(
|
|
216
|
+
msg,
|
|
217
|
+
lambda: bot.timeout(msg.server_id, user_id, minutes, reason),
|
|
218
|
+
f"🔇 Timed out `{user_id}` for {duration}. Reason: {reason}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def unmute(bot, msg):
|
|
223
|
+
"""Remove a timeout. Usage: /unmute <user_id>."""
|
|
224
|
+
args = _args(msg)
|
|
225
|
+
|
|
226
|
+
if not args:
|
|
227
|
+
return _reply(msg, "Usage: /unmute <user_id>")
|
|
228
|
+
if not _server_only(msg):
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
user_id = args[0]
|
|
232
|
+
_run(
|
|
233
|
+
msg,
|
|
234
|
+
lambda: bot.timeout(msg.server_id, user_id, 0, "Timeout removed"),
|
|
235
|
+
f"🔊 Removed timeout for `{user_id}`."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def warn(bot, msg):
|
|
240
|
+
"""Record a warning. Usage: /warn <user_id> <reason>."""
|
|
241
|
+
args = _args(msg)
|
|
242
|
+
|
|
243
|
+
if len(args) < 2:
|
|
244
|
+
return _reply(msg, "Usage: /warn <user_id> <reason>")
|
|
245
|
+
if not _server_only(msg):
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
user_id = str(args[0])
|
|
249
|
+
reason = " ".join(args[1:])
|
|
250
|
+
key = (str(msg.server_id), user_id)
|
|
251
|
+
|
|
252
|
+
_warnings.setdefault(key, []).append(reason)
|
|
253
|
+
count = len(_warnings[key])
|
|
254
|
+
|
|
255
|
+
log_moderation("WARN", msg.author_id, user_id, reason)
|
|
256
|
+
_reply(
|
|
257
|
+
msg,
|
|
258
|
+
f"⚠️ Warned `{user_id}`. Warnings: {count}. Reason: {reason}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def warnings(bot, msg):
|
|
263
|
+
"""Show recorded warnings. Usage: /warnings <user_id>."""
|
|
264
|
+
args = _args(msg)
|
|
265
|
+
|
|
266
|
+
if not args:
|
|
267
|
+
return _reply(msg, "Usage: /warnings <user_id>")
|
|
268
|
+
if not _server_only(msg):
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
user_id = str(args[0])
|
|
272
|
+
items = _warnings.get((str(msg.server_id), user_id), [])
|
|
273
|
+
|
|
274
|
+
if not items:
|
|
275
|
+
return _reply(msg, f"✅ `{user_id}` has no recorded warnings.")
|
|
276
|
+
|
|
277
|
+
listing = "\n".join(
|
|
278
|
+
f"{number}. {reason}"
|
|
279
|
+
for number, reason in enumerate(items, start=1)
|
|
280
|
+
)
|
|
281
|
+
_reply(msg, f"⚠️ Warnings for `{user_id}`:\n{listing}")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def clearwarnings(bot, msg):
|
|
285
|
+
"""Clear recorded warnings. Usage: /clearwarnings <user_id>."""
|
|
286
|
+
args = _args(msg)
|
|
287
|
+
|
|
288
|
+
if not args:
|
|
289
|
+
return _reply(msg, "Usage: /clearwarnings <user_id>")
|
|
290
|
+
if not _server_only(msg):
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
user_id = str(args[0])
|
|
294
|
+
key = (str(msg.server_id), user_id)
|
|
295
|
+
removed = len(_warnings.pop(key, []))
|
|
296
|
+
|
|
297
|
+
log_moderation("CLEAR WARNINGS", msg.author_id, user_id)
|
|
298
|
+
_reply(msg, f"✅ Cleared {removed} warning(s) for `{user_id}`.")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ==================================================
|
|
302
|
+
# PERFORMANCE
|
|
303
|
+
# ==================================================
|
|
304
|
+
|
|
305
|
+
def get_latency(start_time):
|
|
306
|
+
"""Return elapsed time in milliseconds since start_time."""
|
|
307
|
+
return round((time.perf_counter() - start_time) * 1000)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ==================================================
|
|
311
|
+
# MESSAGE HELPERS
|
|
312
|
+
# ==================================================
|
|
313
|
+
|
|
314
|
+
def get_arguments(msg):
|
|
315
|
+
"""Return command arguments as a list."""
|
|
316
|
+
argv = getattr(msg, "argv", None)
|
|
317
|
+
|
|
318
|
+
if argv is not None:
|
|
319
|
+
return list(argv)
|
|
320
|
+
|
|
321
|
+
content = getattr(msg, "content", "") or ""
|
|
322
|
+
return content.split()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def get_argument_text(msg):
|
|
326
|
+
"""Return everything after the command name."""
|
|
327
|
+
return getattr(msg, "args", "") or ""
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def send_message(bot, message, channel_id=None, msg=None):
|
|
331
|
+
"""Send a message to a channel or the command's channel."""
|
|
332
|
+
if channel_id is None and msg is not None:
|
|
333
|
+
channel_id = msg.channel_id
|
|
334
|
+
|
|
335
|
+
if channel_id is None:
|
|
336
|
+
raise ValueError("A channel ID or message object is required.")
|
|
337
|
+
|
|
338
|
+
return bot.send(channel_id, message)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ==================================================
|
|
342
|
+
# SERVER HELPERS
|
|
343
|
+
# ==================================================
|
|
344
|
+
|
|
345
|
+
def get_server_owner_id(bot, msg):
|
|
346
|
+
"""Get the owner ID of the server where the command was used."""
|
|
347
|
+
if not getattr(msg, "server_id", None):
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
server = bot.server(msg.server_id)
|
|
351
|
+
return server.get("owner_id")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def is_server_owner(bot, msg):
|
|
355
|
+
"""Check whether the person using the command owns the server."""
|
|
356
|
+
owner_id = get_server_owner_id(bot, msg)
|
|
357
|
+
|
|
358
|
+
if owner_id is None:
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
return str(msg.author_id) == str(owner_id)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ==================================================
|
|
365
|
+
# ARGUMENT AND REASON HELPERS
|
|
366
|
+
# ==================================================
|
|
367
|
+
|
|
368
|
+
def get_reason(arguments, start=1):
|
|
369
|
+
"""Get a moderation reason from a list of command arguments."""
|
|
370
|
+
reason = " ".join(arguments[start:]).strip()
|
|
371
|
+
return reason or "No reason provided"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def parse_target_and_reason(arguments):
|
|
375
|
+
"""Return a target user ID and reason from command arguments."""
|
|
376
|
+
if not arguments:
|
|
377
|
+
return None, "No reason provided"
|
|
378
|
+
|
|
379
|
+
return arguments[0], get_reason(arguments)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ==================================================
|
|
383
|
+
# DURATION HELPERS
|
|
384
|
+
# ==================================================
|
|
385
|
+
|
|
386
|
+
def parse_duration(value):
|
|
387
|
+
"""
|
|
388
|
+
Convert a duration into seconds.
|
|
389
|
+
|
|
390
|
+
Examples:
|
|
391
|
+
parse_duration("30s") -> 30
|
|
392
|
+
parse_duration("10m") -> 600
|
|
393
|
+
parse_duration("2h") -> 7200
|
|
394
|
+
parse_duration("1d") -> 86400
|
|
395
|
+
"""
|
|
396
|
+
match = re.fullmatch(r"(\d+)([smhd]?)", str(value).lower().strip())
|
|
397
|
+
|
|
398
|
+
if not match:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
amount = int(match.group(1))
|
|
402
|
+
unit = match.group(2) or "s"
|
|
403
|
+
seconds = amount * {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
|
|
404
|
+
|
|
405
|
+
return seconds if amount > 0 else None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def format_duration(seconds):
|
|
409
|
+
"""Format a duration in seconds into a readable string."""
|
|
410
|
+
seconds = max(0, int(seconds))
|
|
411
|
+
|
|
412
|
+
units = (
|
|
413
|
+
("day", 86400),
|
|
414
|
+
("hour", 3600),
|
|
415
|
+
("minute", 60),
|
|
416
|
+
("second", 1),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
parts = []
|
|
420
|
+
|
|
421
|
+
for name, size in units:
|
|
422
|
+
amount, seconds = divmod(seconds, size)
|
|
423
|
+
|
|
424
|
+
if amount:
|
|
425
|
+
label = name if amount == 1 else f"{name}s"
|
|
426
|
+
parts.append(f"{amount} {label}")
|
|
427
|
+
|
|
428
|
+
return ", ".join(parts) if parts else "0 seconds"
|
|
429
|
+
|
|
430
|
+
#//////Other//////
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def embed(title, description=None, color=0x5865F2, fields=None, footer=None):
|
|
434
|
+
"""Create a reusable embed dictionary for Yapper messages."""
|
|
435
|
+
result = {
|
|
436
|
+
"title": str(title),
|
|
437
|
+
"color": color,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if description:
|
|
441
|
+
result["description"] = str(description)
|
|
442
|
+
|
|
443
|
+
if fields:
|
|
444
|
+
result["fields"] = fields
|
|
445
|
+
|
|
446
|
+
if footer:
|
|
447
|
+
result["footer"] = {"text": str(footer)}
|
|
448
|
+
|
|
449
|
+
return result
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def send_embed(msg, title, description=None, color=0x5865F2,
|
|
453
|
+
fields=None, footer="Ezybot"):
|
|
454
|
+
"""Create and send an embed as a reply to a message."""
|
|
455
|
+
msg.reply(
|
|
456
|
+
" ",
|
|
457
|
+
embeds=[embed(title, description, color, fields, footer)]
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def dm(bot, user_id, message):
|
|
461
|
+
"""Open a DM and send a message."""
|
|
462
|
+
try:
|
|
463
|
+
channel_id = bot.dm(user_id)
|
|
464
|
+
bot.send(channel_id, str(message))
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
log_error(f"Failed to DM user {user_id}: {e}")
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
def require_owner(bot, msg):
|
|
472
|
+
"""Return True if the server owner is running the command."""
|
|
473
|
+
if not is_server_owner(bot, msg):
|
|
474
|
+
msg.reply("❌ Only the server owner can use this command.")
|
|
475
|
+
return False
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
def get_target(bot, msg):
|
|
479
|
+
"""Extract a target user ID from the first command argument."""
|
|
480
|
+
import re
|
|
481
|
+
|
|
482
|
+
args = _args(msg)
|
|
483
|
+
|
|
484
|
+
if not args:
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
target = args[0]
|
|
488
|
+
|
|
489
|
+
# Support mentions such as <@123456> and <@!123456>
|
|
490
|
+
match = re.fullmatch(r"<@!?([^<>]+)>", target)
|
|
491
|
+
|
|
492
|
+
if match:
|
|
493
|
+
return match.group(1)
|
|
494
|
+
|
|
495
|
+
# Otherwise, assume the argument is a raw user ID
|
|
496
|
+
return target
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _get_name(data, *keys):
|
|
500
|
+
"""Find the first available name field in an API response."""
|
|
501
|
+
if not data:
|
|
502
|
+
return None
|
|
503
|
+
|
|
504
|
+
for key in keys:
|
|
505
|
+
value = data.get(key)
|
|
506
|
+
|
|
507
|
+
if value and isinstance(value, str):
|
|
508
|
+
return value
|
|
509
|
+
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def format_name(name, item_id=None, show_id=False):
|
|
514
|
+
"""Format a name, optionally displaying its ID alongside it."""
|
|
515
|
+
name = str(name or "Unknown")
|
|
516
|
+
|
|
517
|
+
if show_id and item_id:
|
|
518
|
+
return f"{name} ({item_id})"
|
|
519
|
+
|
|
520
|
+
return name
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def get_server_name(bot, server_id):
|
|
524
|
+
"""Resolve a server ID to its name."""
|
|
525
|
+
try:
|
|
526
|
+
server = bot.server(server_id)
|
|
527
|
+
return _get_name(server, "name") or str(server_id)
|
|
528
|
+
except Exception:
|
|
529
|
+
return str(server_id)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def get_user_name(bot, user_id):
|
|
533
|
+
"""Resolve a user ID to their username."""
|
|
534
|
+
try:
|
|
535
|
+
user = bot.user(user_id)
|
|
536
|
+
return _get_name(
|
|
537
|
+
user, "username", "display_name", "name"
|
|
538
|
+
) or str(user_id)
|
|
539
|
+
except Exception:
|
|
540
|
+
return str(user_id)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def get_role_name(bot, server_id, role_id):
|
|
544
|
+
"""Resolve a role ID to its name."""
|
|
545
|
+
try:
|
|
546
|
+
for role in bot.roles(server_id):
|
|
547
|
+
if str(role.get("id")) == str(role_id):
|
|
548
|
+
return _get_name(role, "name") or str(role_id)
|
|
549
|
+
|
|
550
|
+
return str(role_id)
|
|
551
|
+
except Exception:
|
|
552
|
+
return str(role_id)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def server_label(bot, server_id, show_id=False):
|
|
556
|
+
"""Return a readable server name, optionally with its ID."""
|
|
557
|
+
name = get_server_name(bot, server_id)
|
|
558
|
+
return format_name(name, server_id, show_id)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def user_label(bot, user_id, show_id=False):
|
|
562
|
+
"""Return a readable username, optionally with its ID."""
|
|
563
|
+
name = get_user_name(bot, user_id)
|
|
564
|
+
return format_name(name, user_id, show_id)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def role_label(bot, server_id, role_id, show_id=False):
|
|
568
|
+
"""Return a readable role name, optionally with its ID."""
|
|
569
|
+
name = get_role_name(bot, server_id, role_id)
|
|
570
|
+
return format_name(name, role_id, show_id)
|
|
571
|
+
|
|
572
|
+
def success_embed(msg, title, description=None):
|
|
573
|
+
"""Send a standard green success embed."""
|
|
574
|
+
send_embed(
|
|
575
|
+
msg,
|
|
576
|
+
title=f"✅ {title}",
|
|
577
|
+
description=description,
|
|
578
|
+
color=0x57F287
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def error_embed(msg, title, description=None):
|
|
582
|
+
"""Send a standard red error embed."""
|
|
583
|
+
send_embed(
|
|
584
|
+
msg,
|
|
585
|
+
title=f"❌ {title}",
|
|
586
|
+
description=description,
|
|
587
|
+
color=0xED4245
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
# ==================================================
|
|
593
|
+
# COOLDOWNS
|
|
594
|
+
# ==================================================
|
|
595
|
+
|
|
596
|
+
_cooldowns = {}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def cooldown(user_id, command, seconds=5):
|
|
600
|
+
"""Return remaining cooldown seconds, or 0 if ready."""
|
|
601
|
+
now = time.monotonic()
|
|
602
|
+
|
|
603
|
+
if seconds < 0:
|
|
604
|
+
raise ValueError("Cooldown duration cannot be negative")
|
|
605
|
+
|
|
606
|
+
key = (str(user_id), str(command))
|
|
607
|
+
expires = _cooldowns.get(key, 0)
|
|
608
|
+
|
|
609
|
+
if now < expires:
|
|
610
|
+
return round(expires - now, 1)
|
|
611
|
+
|
|
612
|
+
_cooldowns[key] = now + seconds
|
|
613
|
+
|
|
614
|
+
# Remove expired cooldowns to prevent unlimited dictionary growth
|
|
615
|
+
if len(_cooldowns) > 1000:
|
|
616
|
+
_cleanup_cooldowns(now)
|
|
617
|
+
|
|
618
|
+
return 0
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _cleanup_cooldowns(now):
|
|
622
|
+
"""Remove expired cooldown entries."""
|
|
623
|
+
expired = [
|
|
624
|
+
key for key, expires in _cooldowns.items()
|
|
625
|
+
if expires <= now
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
for key in expired:
|
|
629
|
+
del _cooldowns[key]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ezytools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python utility library for Yapper bot developers
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# Ezytools 🛠️
|
|
11
|
+
|
|
12
|
+
**Reusable Python utilities for bot development**
|
|
13
|
+
|
|
14
|
+
Ezytools is a Python utility library designed to make bot development simpler by handling repetitive tasks in reusable functions. It currently includes moderation helpers built to work with the Yapper Python SDK.
|
|
15
|
+
|
|
16
|
+
Instead of writing the same command logic over and over, you can call an Ezytools function and let it handle argument parsing, basic validation, API calls, and user-facing responses.
|
|
17
|
+
|
|
18
|
+
## ✨ Features
|
|
19
|
+
|
|
20
|
+
- 🛡️ **Moderation commands** — kick, ban, unban, mute, and unmute users
|
|
21
|
+
- ⚠️ **Warning system** — warn users, view their warnings, and clear warnings
|
|
22
|
+
- 🧩 **Yapper SDK integration** — designed to work with your existing Yapper bot
|
|
23
|
+
- 💬 **Argument handling** — parses command arguments and supports quoted text where possible
|
|
24
|
+
- ⏱️ **Duration parsing** — supports minute, hour, and day formats for timeouts
|
|
25
|
+
- 🛠️ **Reusable functions** — keep your bot's main file cleaner and easier to maintain
|
|
26
|
+
- 🚨 **Error handling** — provides readable responses for common moderation errors
|
|
27
|
+
|
|
28
|
+
## 📦 Project Structure
|
|
29
|
+
|
|
30
|
+
Your project can be organised like this:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
your-project/
|
|
34
|
+
├── ezytools.py
|
|
35
|
+
├── yapper.py
|
|
36
|
+
└── ezybot.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `ezytools.py` contains the reusable utility functions
|
|
40
|
+
- `yapper.py` provides the Yapper Python SDK
|
|
41
|
+
- `ezybot.py` contains your bot's commands and main logic
|
|
42
|
+
|
|
43
|
+
## ⚙️ Requirements
|
|
44
|
+
|
|
45
|
+
- Python 3.10 or later recommended
|
|
46
|
+
- The Yapper Python SDK for Yapper-specific moderation helpers
|
|
47
|
+
|
|
48
|
+
Ezytools uses Python's standard library for its current functionality.
|
|
49
|
+
|
|
50
|
+
## 🚀 Installation
|
|
51
|
+
|
|
52
|
+
### 1. Download Ezytools
|
|
53
|
+
|
|
54
|
+
Clone this repository:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone YOUR_REPOSITORY_URL
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Replace `YOUR_REPOSITORY_URL` with the actual URL of this repository.
|
|
61
|
+
|
|
62
|
+
### 2. Add it to your project
|
|
63
|
+
|
|
64
|
+
Place `ezytools.py` in the same directory as your bot file.
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
your-project/
|
|
68
|
+
├── ezytools.py
|
|
69
|
+
├── yapper.py
|
|
70
|
+
└── ezybot.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3. Import Ezytools
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import ezytools
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🛡️ Moderation Utilities
|
|
80
|
+
|
|
81
|
+
Ezytools currently provides these moderation helpers:
|
|
82
|
+
|
|
83
|
+
| Function | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `ezytools.kick(bot, msg)` | Kicks a user from the server |
|
|
86
|
+
| `ezytools.ban(bot, msg)` | Bans a user |
|
|
87
|
+
| `ezytools.unban(bot, msg)` | Removes a user's ban |
|
|
88
|
+
| `ezytools.mute(bot, msg)` | Times out a user |
|
|
89
|
+
| `ezytools.unmute(bot, msg)` | Removes a user's timeout |
|
|
90
|
+
| `ezytools.warn(bot, msg)` | Records a warning for a user |
|
|
91
|
+
| `ezytools.warnings(bot, msg)` | Displays a user's recorded warnings |
|
|
92
|
+
| `ezytools.clearwarnings(bot, msg)` | Clears a user's recorded warnings |
|
|
93
|
+
| `ezytools.help_text()` | Returns a list of the moderation commands |
|
|
94
|
+
|
|
95
|
+
## 💻 Usage
|
|
96
|
+
|
|
97
|
+
Ezytools is designed to work with command handlers from the Yapper Python SDK.
|
|
98
|
+
|
|
99
|
+
### Example: Registering a Kick Command
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import ezytools
|
|
103
|
+
|
|
104
|
+
@bot.command("kick", "Kick a member")
|
|
105
|
+
def kick_command(msg):
|
|
106
|
+
ezytools.kick(bot, msg)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Example: Registering Moderation Commands
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import ezytools
|
|
113
|
+
|
|
114
|
+
@bot.command("kick", "Kick a member")
|
|
115
|
+
def kick_command(msg):
|
|
116
|
+
ezytools.kick(bot, msg)
|
|
117
|
+
|
|
118
|
+
@bot.command("ban", "Ban a member")
|
|
119
|
+
def ban_command(msg):
|
|
120
|
+
ezytools.ban(bot, msg)
|
|
121
|
+
|
|
122
|
+
@bot.command("unban", "Unban a user")
|
|
123
|
+
def unban_command(msg):
|
|
124
|
+
ezytools.unban(bot, msg)
|
|
125
|
+
|
|
126
|
+
@bot.command("mute", "Timeout a member")
|
|
127
|
+
def mute_command(msg):
|
|
128
|
+
ezytools.mute(bot, msg)
|
|
129
|
+
|
|
130
|
+
@bot.command("unmute", "Remove a member's timeout")
|
|
131
|
+
def unmute_command(msg):
|
|
132
|
+
ezytools.unmute(bot, msg)
|
|
133
|
+
|
|
134
|
+
@bot.command("warn", "Warn a member")
|
|
135
|
+
def warn_command(msg):
|
|
136
|
+
ezytools.warn(bot, msg)
|
|
137
|
+
|
|
138
|
+
@bot.command("warnings", "View a member's warnings")
|
|
139
|
+
def warnings_command(msg):
|
|
140
|
+
ezytools.warnings(bot, msg)
|
|
141
|
+
|
|
142
|
+
@bot.command("clearwarnings", "Clear a member's warnings")
|
|
143
|
+
def clearwarnings_command(msg):
|
|
144
|
+
ezytools.clearwarnings(bot, msg)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Important:** This example assumes you have already created your Yapper `bot` instance. The bot must have the permissions required for each moderation action.
|
|
148
|
+
|
|
149
|
+
## ⏱️ Timeout Durations
|
|
150
|
+
|
|
151
|
+
The mute helper accepts durations in these formats:
|
|
152
|
+
|
|
153
|
+
| Format | Meaning |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `10m` | 10 minutes |
|
|
156
|
+
| `2h` | 2 hours |
|
|
157
|
+
| `1d` | 1 day |
|
|
158
|
+
| `30` | 30 minutes |
|
|
159
|
+
|
|
160
|
+
The maximum supported duration is **28 days**.
|
|
161
|
+
|
|
162
|
+
Example command:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
/mute 123456789 30m Spamming
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Replace the example user ID with the actual user's ID.
|
|
169
|
+
|
|
170
|
+
## ⚠️ Warning System
|
|
171
|
+
|
|
172
|
+
Warnings are currently stored **in memory**.
|
|
173
|
+
|
|
174
|
+
This means:
|
|
175
|
+
- Warnings can be viewed using the warnings helper
|
|
176
|
+
- Warnings can be cleared using the clearwarnings helper
|
|
177
|
+
- Warnings are separated by server and user
|
|
178
|
+
- **Warnings are lost when the bot restarts**
|
|
179
|
+
|
|
180
|
+
Persistent warning storage would require a database or file-based storage system.
|
|
181
|
+
|
|
182
|
+
## 🧰 Error Handling
|
|
183
|
+
|
|
184
|
+
Ezytools handles common errors returned by moderation requests, including:
|
|
185
|
+
|
|
186
|
+
- Missing permissions
|
|
187
|
+
- Users or bans that cannot be found
|
|
188
|
+
- Invalid requests
|
|
189
|
+
- Incorrect command arguments
|
|
190
|
+
|
|
191
|
+
The bot still needs the correct server permissions, and the Yapper SDK must be configured correctly.
|
|
192
|
+
|
|
193
|
+
## 🔒 Security
|
|
194
|
+
|
|
195
|
+
- Keep your bot token private
|
|
196
|
+
- Store sensitive configuration in environment variables
|
|
197
|
+
- Never commit tokens or passwords to GitHub
|
|
198
|
+
- Give your bot only the permissions it needs
|
|
199
|
+
- Test moderation commands in a safe test server before using them in a real community
|
|
200
|
+
|
|
201
|
+
## 🛠️ Troubleshooting
|
|
202
|
+
|
|
203
|
+
### Ezytools cannot be imported
|
|
204
|
+
|
|
205
|
+
Make sure `ezytools.py` is in the same directory as your bot file, or installed in a location Python can import from.
|
|
206
|
+
|
|
207
|
+
### Moderation commands fail
|
|
208
|
+
|
|
209
|
+
Check that:
|
|
210
|
+
- The bot is running and connected
|
|
211
|
+
- The bot has the required server permissions
|
|
212
|
+
- The user ID is correct
|
|
213
|
+
- The command is being used inside a server
|
|
214
|
+
|
|
215
|
+
### Timeout duration is rejected
|
|
216
|
+
|
|
217
|
+
Use a valid duration such as `10m`, `2h`, `1d`, or a number of minutes. The maximum is 28 days.
|
|
218
|
+
|
|
219
|
+
### Warnings disappear
|
|
220
|
+
|
|
221
|
+
Warnings are stored in memory, so restarting the bot clears them.
|
|
222
|
+
|
|
223
|
+
## 🤝 Contributing
|
|
224
|
+
|
|
225
|
+
Contributions, bug reports, and suggestions are welcome.
|
|
226
|
+
|
|
227
|
+
When contributing:
|
|
228
|
+
1. Keep functions reusable
|
|
229
|
+
2. Follow standard Python formatting conventions
|
|
230
|
+
3. Test changes before submitting them
|
|
231
|
+
4. Update the README when features or usage change
|
|
232
|
+
|
|
233
|
+
## 📄 License
|
|
234
|
+
|
|
235
|
+
No license has been specified yet.
|
|
236
|
+
|
|
237
|
+
If you plan to make Ezytools publicly reusable, add a license file before publishing it so users know how they are allowed to use, modify, and distribute the code.
|
|
238
|
+
|
|
239
|
+
## 📌 About
|
|
240
|
+
|
|
241
|
+
Ezytools is a developing Python utility library focused on making bot development cleaner, simpler, and more reusable.
|
|
242
|
+
|
|
243
|
+
**Current focus:** moderation helpers and Yapper SDK integration.
|
|
244
|
+
|
|
245
|
+
More utilities can be added as the library develops.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ezytools/_init_.py,sha256=KF74cM2LTYxZmZTUo38tuXCkepXWVO1WwWvm3tQV7wo,46
|
|
2
|
+
ezytools/core.py,sha256=qcJc_SlVFcY-SPAavfwXFAsH0NMaTrAwUfA_c2KUBpg,15697
|
|
3
|
+
ezytools-0.1.0.dist-info/METADATA,sha256=0mcQLEwWep9HZCjz9OABhK1ljmTahQUKbbH-t5ixhMQ,6898
|
|
4
|
+
ezytools-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
5
|
+
ezytools-0.1.0.dist-info/licenses/LICENSE,sha256=8IYNN526nDa6NJPEa4u2USFvNkVN9c7cITECXpY4kXA,2180
|
|
6
|
+
ezytools-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# EZYTOOLS PROPRIETARY LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Ezytools. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Ezytools, including its original source code, documentation, design, and associated materials, is the intellectual property of its creator.
|
|
6
|
+
|
|
7
|
+
## 1. Ownership
|
|
8
|
+
|
|
9
|
+
All rights, title, and interest in Ezytools remain with its creator, to the extent permitted by applicable law. Nothing in this license transfers ownership of the software to another person or organisation.
|
|
10
|
+
|
|
11
|
+
## 2. Restrictions
|
|
12
|
+
|
|
13
|
+
Without prior written permission from the creator, you may not:
|
|
14
|
+
|
|
15
|
+
- Copy, redistribute, republish, or resell the Ezytools source code.
|
|
16
|
+
- Modify, rebrand, or distribute derivative versions of Ezytools.
|
|
17
|
+
- Claim Ezytools or its original code as your own work.
|
|
18
|
+
- Publish or distribute copies of the source code outside authorised project channels.
|
|
19
|
+
- Sell, sublicense, or commercially redistribute Ezytools or copies of it.
|
|
20
|
+
|
|
21
|
+
## 3. Limited Permission
|
|
22
|
+
|
|
23
|
+
Viewing or accessing Ezytools does not automatically grant permission to copy, modify, redistribute, or commercially use the software.
|
|
24
|
+
|
|
25
|
+
Any additional permissions must be explicitly authorised in writing by the creator.
|
|
26
|
+
|
|
27
|
+
## 4. Third-Party Materials
|
|
28
|
+
|
|
29
|
+
This license applies only to materials the creator has the right to license. Third-party code, libraries, documentation, and other materials remain subject to their respective licences and terms.
|
|
30
|
+
|
|
31
|
+
## 5. Contributions
|
|
32
|
+
|
|
33
|
+
Unless agreed otherwise in writing, contributors retain any rights they hold in their contributions. The terms governing the use and distribution of contributions must be agreed upon by the creator and contributor.
|
|
34
|
+
|
|
35
|
+
## 6. Termination
|
|
36
|
+
|
|
37
|
+
Any permission granted under a separate written agreement may be terminated in accordance with that agreement and applicable law if its conditions are violated.
|
|
38
|
+
|
|
39
|
+
## 7. Disclaimer
|
|
40
|
+
|
|
41
|
+
Ezytools is provided "as is", without warranties of any kind, to the fullest extent permitted by applicable law.
|
|
42
|
+
|
|
43
|
+
## 8. Contact and Permissions
|
|
44
|
+
|
|
45
|
+
For permission to use, modify, redistribute, or commercially license Ezytools, contact its creator through the official Ezytools project channels.
|
|
46
|
+
|
|
47
|
+
No additional rights are granted except those explicitly stated in writing by the creator.
|