ezytools 0.1.0__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.
ezytools-0.1.0/LICENSE ADDED
@@ -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.
@@ -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,236 @@
1
+ # Ezytools 🛠️
2
+
3
+ **Reusable Python utilities for bot development**
4
+
5
+ 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.
6
+
7
+ 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.
8
+
9
+ ## ✨ Features
10
+
11
+ - 🛡️ **Moderation commands** — kick, ban, unban, mute, and unmute users
12
+ - ⚠️ **Warning system** — warn users, view their warnings, and clear warnings
13
+ - 🧩 **Yapper SDK integration** — designed to work with your existing Yapper bot
14
+ - 💬 **Argument handling** — parses command arguments and supports quoted text where possible
15
+ - ⏱️ **Duration parsing** — supports minute, hour, and day formats for timeouts
16
+ - 🛠️ **Reusable functions** — keep your bot's main file cleaner and easier to maintain
17
+ - 🚨 **Error handling** — provides readable responses for common moderation errors
18
+
19
+ ## 📦 Project Structure
20
+
21
+ Your project can be organised like this:
22
+
23
+ ```text
24
+ your-project/
25
+ ├── ezytools.py
26
+ ├── yapper.py
27
+ └── ezybot.py
28
+ ```
29
+
30
+ - `ezytools.py` contains the reusable utility functions
31
+ - `yapper.py` provides the Yapper Python SDK
32
+ - `ezybot.py` contains your bot's commands and main logic
33
+
34
+ ## ⚙️ Requirements
35
+
36
+ - Python 3.10 or later recommended
37
+ - The Yapper Python SDK for Yapper-specific moderation helpers
38
+
39
+ Ezytools uses Python's standard library for its current functionality.
40
+
41
+ ## 🚀 Installation
42
+
43
+ ### 1. Download Ezytools
44
+
45
+ Clone this repository:
46
+
47
+ ```bash
48
+ git clone YOUR_REPOSITORY_URL
49
+ ```
50
+
51
+ Replace `YOUR_REPOSITORY_URL` with the actual URL of this repository.
52
+
53
+ ### 2. Add it to your project
54
+
55
+ Place `ezytools.py` in the same directory as your bot file.
56
+
57
+ ```text
58
+ your-project/
59
+ ├── ezytools.py
60
+ ├── yapper.py
61
+ └── ezybot.py
62
+ ```
63
+
64
+ ### 3. Import Ezytools
65
+
66
+ ```python
67
+ import ezytools
68
+ ```
69
+
70
+ ## 🛡️ Moderation Utilities
71
+
72
+ Ezytools currently provides these moderation helpers:
73
+
74
+ | Function | Description |
75
+ |---|---|
76
+ | `ezytools.kick(bot, msg)` | Kicks a user from the server |
77
+ | `ezytools.ban(bot, msg)` | Bans a user |
78
+ | `ezytools.unban(bot, msg)` | Removes a user's ban |
79
+ | `ezytools.mute(bot, msg)` | Times out a user |
80
+ | `ezytools.unmute(bot, msg)` | Removes a user's timeout |
81
+ | `ezytools.warn(bot, msg)` | Records a warning for a user |
82
+ | `ezytools.warnings(bot, msg)` | Displays a user's recorded warnings |
83
+ | `ezytools.clearwarnings(bot, msg)` | Clears a user's recorded warnings |
84
+ | `ezytools.help_text()` | Returns a list of the moderation commands |
85
+
86
+ ## 💻 Usage
87
+
88
+ Ezytools is designed to work with command handlers from the Yapper Python SDK.
89
+
90
+ ### Example: Registering a Kick Command
91
+
92
+ ```python
93
+ import ezytools
94
+
95
+ @bot.command("kick", "Kick a member")
96
+ def kick_command(msg):
97
+ ezytools.kick(bot, msg)
98
+ ```
99
+
100
+ ### Example: Registering Moderation Commands
101
+
102
+ ```python
103
+ import ezytools
104
+
105
+ @bot.command("kick", "Kick a member")
106
+ def kick_command(msg):
107
+ ezytools.kick(bot, msg)
108
+
109
+ @bot.command("ban", "Ban a member")
110
+ def ban_command(msg):
111
+ ezytools.ban(bot, msg)
112
+
113
+ @bot.command("unban", "Unban a user")
114
+ def unban_command(msg):
115
+ ezytools.unban(bot, msg)
116
+
117
+ @bot.command("mute", "Timeout a member")
118
+ def mute_command(msg):
119
+ ezytools.mute(bot, msg)
120
+
121
+ @bot.command("unmute", "Remove a member's timeout")
122
+ def unmute_command(msg):
123
+ ezytools.unmute(bot, msg)
124
+
125
+ @bot.command("warn", "Warn a member")
126
+ def warn_command(msg):
127
+ ezytools.warn(bot, msg)
128
+
129
+ @bot.command("warnings", "View a member's warnings")
130
+ def warnings_command(msg):
131
+ ezytools.warnings(bot, msg)
132
+
133
+ @bot.command("clearwarnings", "Clear a member's warnings")
134
+ def clearwarnings_command(msg):
135
+ ezytools.clearwarnings(bot, msg)
136
+ ```
137
+
138
+ **Important:** This example assumes you have already created your Yapper `bot` instance. The bot must have the permissions required for each moderation action.
139
+
140
+ ## ⏱️ Timeout Durations
141
+
142
+ The mute helper accepts durations in these formats:
143
+
144
+ | Format | Meaning |
145
+ |---|---|
146
+ | `10m` | 10 minutes |
147
+ | `2h` | 2 hours |
148
+ | `1d` | 1 day |
149
+ | `30` | 30 minutes |
150
+
151
+ The maximum supported duration is **28 days**.
152
+
153
+ Example command:
154
+
155
+ ```text
156
+ /mute 123456789 30m Spamming
157
+ ```
158
+
159
+ Replace the example user ID with the actual user's ID.
160
+
161
+ ## ⚠️ Warning System
162
+
163
+ Warnings are currently stored **in memory**.
164
+
165
+ This means:
166
+ - Warnings can be viewed using the warnings helper
167
+ - Warnings can be cleared using the clearwarnings helper
168
+ - Warnings are separated by server and user
169
+ - **Warnings are lost when the bot restarts**
170
+
171
+ Persistent warning storage would require a database or file-based storage system.
172
+
173
+ ## 🧰 Error Handling
174
+
175
+ Ezytools handles common errors returned by moderation requests, including:
176
+
177
+ - Missing permissions
178
+ - Users or bans that cannot be found
179
+ - Invalid requests
180
+ - Incorrect command arguments
181
+
182
+ The bot still needs the correct server permissions, and the Yapper SDK must be configured correctly.
183
+
184
+ ## 🔒 Security
185
+
186
+ - Keep your bot token private
187
+ - Store sensitive configuration in environment variables
188
+ - Never commit tokens or passwords to GitHub
189
+ - Give your bot only the permissions it needs
190
+ - Test moderation commands in a safe test server before using them in a real community
191
+
192
+ ## 🛠️ Troubleshooting
193
+
194
+ ### Ezytools cannot be imported
195
+
196
+ Make sure `ezytools.py` is in the same directory as your bot file, or installed in a location Python can import from.
197
+
198
+ ### Moderation commands fail
199
+
200
+ Check that:
201
+ - The bot is running and connected
202
+ - The bot has the required server permissions
203
+ - The user ID is correct
204
+ - The command is being used inside a server
205
+
206
+ ### Timeout duration is rejected
207
+
208
+ Use a valid duration such as `10m`, `2h`, `1d`, or a number of minutes. The maximum is 28 days.
209
+
210
+ ### Warnings disappear
211
+
212
+ Warnings are stored in memory, so restarting the bot clears them.
213
+
214
+ ## 🤝 Contributing
215
+
216
+ Contributions, bug reports, and suggestions are welcome.
217
+
218
+ When contributing:
219
+ 1. Keep functions reusable
220
+ 2. Follow standard Python formatting conventions
221
+ 3. Test changes before submitting them
222
+ 4. Update the README when features or usage change
223
+
224
+ ## 📄 License
225
+
226
+ No license has been specified yet.
227
+
228
+ 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.
229
+
230
+ ## 📌 About
231
+
232
+ Ezytools is a developing Python utility library focused on making bot development cleaner, simpler, and more reusable.
233
+
234
+ **Current focus:** moderation helpers and Yapper SDK integration.
235
+
236
+ More utilities can be added as the library develops.
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ezytools"
7
+ version = "0.1.0"
8
+ description = "A Python utility library for Yapper bot developers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "LicenseRef-Proprietary"
12
+ license-files = ["LICENSE"]
13
+ dependencies = []
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/ezytools"]
@@ -0,0 +1,3 @@
1
+ from .core import *
2
+
3
+ __version__ = "0.1.0"
@@ -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]