.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 
4 |
5 | Advanced multipurpose discord bot for all your needs.
6 |
7 |
8 |
9 | ## 🎯 Features
10 |
11 | - Advanced moderation system.
12 | - Lots of utility & fun commands.
13 | - Advanced music system with support for YouTube, Spotify and SoundCloud.
14 | - Clean & informative help menu.
15 |
16 | ## 🚩 Installation
17 |
18 | 1. Clone this repository
19 | ```sh
20 | git clone https://github.com/swayam25/Square-Bot square
21 | cd square
22 | ```
23 |
24 | 2. Create `config.toml` from `example.config.toml` and fill in the required values.
25 |
26 |
27 | Configuration
28 |
29 | - `owner-id` (`int`)
30 | - Owner's discord id.
31 | - Gives access to all commands.
32 |
33 | - `owner-guild-ids` (`list[int]`)
34 | - List of guild ids.
35 | - Developer commands will only work in these guilds.
36 |
37 | - `system-channel-id` (`int`)
38 | - System channel id.
39 | - Bot will send logs in this channel.
40 |
41 | - `support-server-url` (`str`)
42 | - Support server url.
43 | - Bot will use this url for support server.
44 |
45 | - `emoji` (`Literal["default", "custom"]`)
46 | - Emoji type.
47 | - `default` will use default emojis.
48 | - `custom` will use custom emojis defined in `.cache/emoji.json` (*requires setting up custom emojis*).
49 | - If you choose `custom`, make sure to define the emojis in the `.cache/emoji.json` file.
50 | - To create custom emojis, upload a `.zip` file containing the emojis (*`.png` format*) using `/emoji upload` command.
51 | - Emoji file names must match the attributes of `Emoji` class in [`emoji.py`](./utils/emoji.py).
52 | - Then run `/emoji sync` command to sync the emojis. (*This creates `.cache/emoji.json` file from bot's emojis*).
53 | - You can also manually create the `.cache/emoji.json` file with the same structure as `Emoji` class in [`emoji.py`](./utils/emoji.py).
54 | - Then set the `emoji` field to `custom`.
55 |
56 | - `bot-token` (`str`)
57 | - Discord api token.
58 | - Bot will use this token to connect to discord.
59 |
60 | - `database-url` (`str`)
61 | - Database url.
62 | - Bot will use this url to connect to the database.
63 | - Postgres database is supported.
64 | - Example: `asyncpg://user:password@db.host:5432/square`.
65 | - If your connection string starts with `postgresql://`, replace it with `asyncpg://`.
66 | - Services like Supabase provide a `postgresql://` connection string, remember to change it to `asyncpg://`.
67 |
68 | - `[colors]`
69 | - `theme` (`str`)
70 | - Theme color.
71 | - `error` (`str`)
72 | - Error color.
73 |
74 | - `[lavalink]`
75 | - `host` (`str`)
76 | - Lavalink host.
77 | - `port` (`int`)
78 | - Lavalink port.
79 | - `password` (`str`)
80 | - Lavalink password.
81 | - `secure` (`bool`)
82 | - Lavalink secure status
83 |
84 | - `[spotify]`
85 | - `client_id` (`str`)
86 | - Spotify client id
87 | - `client_secret` (`str`)
88 | - Spotify client secret
89 |
90 |
91 |
92 | 3. Set spotify credentials in `config.toml` file.
93 | - Go to [Spotify Developer Dashboard](https://developer.spotify.com/dashboard).
94 | - Create a new application (*visit [Spotify Developer Docs](https://developer.spotify.com/documentation/web-api/tutorials/getting-started) for more details*).
95 | - Get the `client_id` and `client_secret` from the application settings.
96 | - Set the `client_id` and `client_secret` in the `config.toml` file.
97 |
98 | 4. Start the bot.
99 | ```sh
100 | uv run main.py
101 | ```
102 |
103 | > [!IMPORTANT]
104 | > Make sure to have [uv](https://docs.astral.sh/uv) installed on your system to run the bot.
105 | > Know more about installing uv [here](https://docs.astral.sh/uv/getting-started/installation/).
106 |
107 | ## 🚀 Production
108 |
109 | 1. Follow steps 1-3 from the [installation guide](#-installation). *Ignore if already done.*
110 |
111 | 2. Run docker container (*via `docker compose`*)
112 | ```sh
113 | docker compose up -d
114 | ```
115 |
116 | ## ❤️ Contributing
117 |
118 | - Things to keep in mind
119 | - Follow our commit message convention.
120 | - Write meaningful commit messages.
121 | - Keep the code clean and readable.
122 | - Make sure the bot is working as expected.
123 |
124 | - Code Formatting
125 | - Run `ruff format` before committing your changes or use [`Ruff`](https://docs.astral.sh/ruff/editors) extension in your code editor.
126 | - Make sure to commit error free code. Run `ruff check` to check for any errors.
127 |
--------------------------------------------------------------------------------
/assets/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swayam25/Square-Bot/a0da721bf769bf2b7fe4c6a8a81ade1bd6ef6695/assets/banner.png
--------------------------------------------------------------------------------
/assets/dc_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swayam25/Square-Bot/a0da721bf769bf2b7fe4c6a8a81ade1bd6ef6695/assets/dc_banner.png
--------------------------------------------------------------------------------
/assets/square.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swayam25/Square-Bot/a0da721bf769bf2b7fe4c6a8a81ade1bd6ef6695/assets/square.png
--------------------------------------------------------------------------------
/cogs/automod.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from db.funcs.guild import fetch_guild_settings, set_autorole
3 | from discord.ext import commands
4 |
5 |
6 | class AutoMod(commands.Cog):
7 | def __init__(self, client):
8 | self.client = client
9 |
10 | # Autorole
11 | @commands.Cog.listener()
12 | async def on_member_join(self, user: discord.Member):
13 | autorole = (await fetch_guild_settings(user.guild.id)).autorole
14 | if not user.bot:
15 | role = user.guild.get_role(autorole)
16 | if role:
17 | await user.add_roles(role)
18 | else:
19 | await set_autorole(user.guild.id, None)
20 |
21 |
22 | def setup(client: discord.Bot):
23 | client.add_cog(AutoMod(client))
24 |
--------------------------------------------------------------------------------
/cogs/devs.py:
--------------------------------------------------------------------------------
1 | import discord
2 | import discord.ui
3 | import math
4 | import os
5 | import sys
6 | import zipfile
7 | from db.funcs.dev import add_dev, fetch_dev_ids, remove_dev
8 | from db.funcs.guild import add_guild, remove_guild
9 | from discord.commands import SlashCommandGroup, option, slash_command
10 | from discord.ext import commands
11 | from io import BytesIO
12 | from utils import check, config
13 | from utils.emoji import Emoji, emoji
14 |
15 |
16 | class GuildListView(discord.ui.View):
17 | def __init__(self, client: discord.Bot, ctx: discord.ApplicationContext, page: int, timeout: int):
18 | super().__init__(timeout=timeout, disable_on_timeout=True)
19 | self.client = client
20 | self.ctx = ctx
21 | self.page = page
22 | self.items_per_page = 10
23 |
24 | async def interaction_check(self, interaction: discord.Interaction):
25 | if interaction.user != self.ctx.author:
26 | help_check_em = discord.Embed(
27 | description=f"{emoji.error} You are not the author of this message", color=config.color.error
28 | )
29 | await interaction.response.send_message(embed=help_check_em, ephemeral=True)
30 | return False
31 | else:
32 | return True
33 |
34 | # Start
35 | @discord.ui.button(emoji=f"{emoji.start}", custom_id="start", style=discord.ButtonStyle.grey)
36 | async def start_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
37 | self.page = 1
38 | em = GuildListEmbed(self.client, self.page).get_embed()
39 | await interaction.response.edit_message(embed=em, view=self)
40 |
41 | # Previous
42 | @discord.ui.button(emoji=f"{emoji.previous}", custom_id="previous", style=discord.ButtonStyle.grey)
43 | async def previous_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
44 | pages = math.ceil(len(self.client.guilds) / self.items_per_page)
45 | if self.page <= 1:
46 | self.page = pages
47 | else:
48 | self.page -= 1
49 | em = GuildListEmbed(self.client, self.page).get_embed()
50 | await interaction.response.edit_message(embed=em, view=self)
51 |
52 | # Next
53 | @discord.ui.button(emoji=f"{emoji.next}", custom_id="next", style=discord.ButtonStyle.grey)
54 | async def next_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
55 | pages = math.ceil(len(self.client.guilds) / self.items_per_page)
56 | if self.page >= pages:
57 | self.page = 1
58 | else:
59 | self.page += 1
60 | em = GuildListEmbed(self.client, self.page).get_embed()
61 | await interaction.response.edit_message(embed=em, view=self)
62 |
63 | # End
64 | @discord.ui.button(emoji=f"{emoji.end}", custom_id="end", style=discord.ButtonStyle.grey)
65 | async def end_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
66 | self.page = math.ceil(len(self.client.guilds) / self.items_per_page)
67 | em = GuildListEmbed(self.client, self.page).get_embed()
68 | await interaction.response.edit_message(embed=em, view=self)
69 |
70 |
71 | class GuildListEmbed(discord.Embed):
72 | def __init__(self, client: discord.Bot, page: int):
73 | super().__init__(title=f"{emoji.embed} Guilds List", color=config.color.theme)
74 | self.client = client
75 | self.page = page
76 | self.items_per_page = 10
77 |
78 | def get_guilds(self):
79 | guilds = self.client.guilds
80 | start = (self.page - 1) * self.items_per_page
81 | end = start + self.items_per_page
82 | return guilds[start:end]
83 |
84 | def get_guilds_list(self):
85 | guilds_list = ""
86 | for num, guild in enumerate(self.get_guilds(), start=self.items_per_page * (self.page - 1) + 1):
87 | guilds_list += f"`{num}.` **{guild.name}** - `{guild.id}`\n"
88 | return guilds_list
89 |
90 | def get_footer(self):
91 | total_pages = math.ceil(len(self.client.guilds) / self.items_per_page)
92 | return f"Viewing Page {self.page}/{total_pages}"
93 |
94 | def get_embed(self):
95 | self.description = self.get_guilds_list()
96 | self.set_footer(text=self.get_footer())
97 | return self
98 |
99 |
100 | class Devs(commands.Cog):
101 | def __init__(self, client: discord.Bot):
102 | self.client = client
103 |
104 | # On start
105 | @commands.Cog.listener("on_ready")
106 | async def when_bot_gets_ready(self):
107 | start_log_ch = await self.client.fetch_channel(config.system_channel_id)
108 | start_log_em = discord.Embed(
109 | title=f"{emoji.restart} Restarted",
110 | description=f"Logged in as **{self.client.user}** with ID `{self.client.user.id}`",
111 | color=config.color.theme,
112 | )
113 | await start_log_ch.send(embed=start_log_em)
114 |
115 | # On guild joined
116 | @commands.Cog.listener("on_guild_join")
117 | async def when_guild_joined(self, guild: discord.Guild):
118 | await add_guild(guild.id)
119 | join_log_ch = await self.client.fetch_channel(config.system_channel_id)
120 | join_log_em = discord.Embed(
121 | title=f"{emoji.plus} Someone Added Me!",
122 | description=f"{emoji.bullet} **Name**: {guild.name}\n"
123 | f"{emoji.bullet} **ID**: `{guild.id}`\n"
124 | f"{emoji.bullet} **Total Members**: `{guild.member_count}`\n"
125 | f"{emoji.bullet} **Total Humans**: `{len([m for m in guild.members if not m.bot])}`\n"
126 | f"{emoji.bullet} **Total Bots**: `{len([m for m in guild.members if m.bot])}`",
127 | color=config.color.theme,
128 | )
129 | await join_log_ch.send(embed=join_log_em)
130 |
131 | # On guild leave
132 | @commands.Cog.listener("on_guild_remove")
133 | async def when_removed_from_guild(self, guild: discord.Guild):
134 | await remove_guild(guild.id)
135 | leave_log_ch = await self.client.fetch_channel(config.system_channel_id)
136 | leave_log_em = discord.Embed(
137 | title=f"{emoji.minus} Someone Removed Me!",
138 | description=f"{emoji.bullet2} **Name**: {guild.name}\n"
139 | f"{emoji.bullet2} **ID**: `{guild.id}`\n"
140 | f"{emoji.bullet2} **Total Members**: `{guild.member_count}`\n"
141 | f"{emoji.bullet2} **Total Humans**: `{len([m for m in guild.members if not m.bot])}`\n"
142 | f"{emoji.bullet2} **Total Bots**: `{len([m for m in guild.members if m.bot])}`",
143 | color=config.color.error,
144 | )
145 | await leave_log_ch.send(embed=leave_log_em)
146 |
147 | # Dev slash cmd group
148 | dev = SlashCommandGroup(guild_ids=config.owner_guild_ids, name="dev", description="Developer related commands.")
149 |
150 | # Add dev
151 | @dev.command(name="add")
152 | @option("user", description="Mention the user whom you want to add to dev")
153 | @check.is_owner()
154 | async def add_dev(self, ctx: discord.ApplicationContext, user: discord.Member):
155 | """Adds a bot dev."""
156 | await add_dev(user.id)
157 | done_em = discord.Embed(
158 | title=f"{emoji.plus} Added", description=f"Added {user.mention} to dev", color=config.color.theme
159 | )
160 | await ctx.respond(embed=done_em)
161 |
162 | # Remove dev
163 | @dev.command(name="remove")
164 | @option("user", description="Mention the user whom you want to remove from dev")
165 | @check.is_owner()
166 | async def remove_dev(self, ctx: discord.ApplicationContext, user: discord.Member):
167 | """Removes a bot dev."""
168 | await remove_dev(user.id)
169 | done_em = discord.Embed(
170 | title=f"{emoji.bin} Removed",
171 | description=f"Removed {user.mention} from dev",
172 | color=config.color.error,
173 | )
174 | await ctx.respond(embed=done_em)
175 |
176 | # List devs
177 | @dev.command(name="list")
178 | @check.is_owner()
179 | async def list_devs(self, ctx: discord.ApplicationContext):
180 | """Shows bot devs."""
181 | num = 0
182 | devs_list = ""
183 | dev_ids = await fetch_dev_ids()
184 | for ids in dev_ids:
185 | num += 1
186 | dev_mention = f"<@{ids}>"
187 | devs_list += f"`{num}.` {dev_mention}\n"
188 | dev_em = discord.Embed(title=f"{emoji.embed} Devs List", description=devs_list, color=config.color.theme)
189 | await ctx.respond(embed=dev_em)
190 |
191 | # Restart
192 | @slash_command(guild_ids=config.owner_guild_ids, name="restart")
193 | @check.is_dev()
194 | async def restart(self, ctx: discord.ApplicationContext):
195 | """Restarts the bot."""
196 | restart_em = discord.Embed(title=f"{emoji.restart} Restarting", color=config.color.theme)
197 | await ctx.respond(embed=restart_em)
198 | await self.client.wait_until_ready()
199 | await self.client.close()
200 | os.system("clear")
201 | os.execv(sys.executable, [sys.executable] + sys.argv)
202 |
203 | # Reload cogs
204 | @slash_command(guild_ids=config.owner_guild_ids, name="reload-cogs")
205 | @check.is_dev()
206 | async def reload_cogs(self, ctx: discord.ApplicationContext):
207 | """Reloads bot's all files."""
208 | reload_em = discord.Embed(title=f"{emoji.restart} Reloaded Cogs", color=config.color.theme)
209 | await ctx.respond(embed=reload_em, ephemeral=True, delete_after=2)
210 | for filename in os.listdir("./cogs"):
211 | if filename.endswith(".py"):
212 | self.client.reload_extension(f"cogs.{filename[:-3]}")
213 |
214 | # Shutdown
215 | @slash_command(guild_ids=config.owner_guild_ids, name="shutdown")
216 | @check.is_owner()
217 | async def shutdown(self, ctx: discord.ApplicationContext):
218 | """Shutdowns the bot."""
219 | shutdown_em = discord.Embed(title=f"{emoji.shutdown} Shutdown", color=config.color.error)
220 | await ctx.respond(embed=shutdown_em)
221 | await self.client.wait_until_ready()
222 | await self.client.close()
223 |
224 | # Set status
225 | @slash_command(guild_ids=config.owner_guild_ids, name="status")
226 | @option("type", description="Choose bot status type", choices=["Game", "Streaming", "Listening", "Watching"])
227 | @option("status", description="Enter new status of bot")
228 | @check.is_dev()
229 | async def set_status(self, ctx: discord.ApplicationContext, type: str, status: str):
230 | """Sets custom bot status."""
231 | if type == "Game":
232 | await self.client.change_presence(activity=discord.Game(name=status))
233 | elif type == "Streaming":
234 | await self.client.change_presence(activity=discord.Streaming(name=status, url=config.support_server_url))
235 | elif type == "Listening":
236 | await self.client.change_presence(
237 | activity=discord.Activity(type=discord.ActivityType.listening, name=status)
238 | )
239 | elif type == "Watching":
240 | await self.client.change_presence(
241 | activity=discord.Activity(type=discord.ActivityType.watching, name=status)
242 | )
243 | status_em = discord.Embed(
244 | title=f"{emoji.console} Status Changed",
245 | description=f"Status changed to **{type}** as `{status}`",
246 | color=config.color.theme,
247 | )
248 | await ctx.respond(embed=status_em)
249 |
250 | # Guild slash cmd group
251 | guild = SlashCommandGroup(guild_ids=config.owner_guild_ids, name="guild", description="Guild related commands.")
252 |
253 | # List guild
254 | @guild.command(name="list")
255 | @check.is_owner()
256 | async def list_guilds(self, ctx: discord.ApplicationContext):
257 | """Shows all guilds."""
258 | guild_list_em = GuildListEmbed(self.client, 1).get_embed()
259 | guild_list_view = None
260 | if len(self.client.guilds) > 10:
261 | guild_list_view = GuildListView(self.client, ctx, 1, timeout=60)
262 | await ctx.respond(embed=guild_list_em, view=guild_list_view)
263 |
264 | # Leave guild
265 | @guild.command(name="leave")
266 | @option(
267 | "guild",
268 | description="Enter the guild name",
269 | autocomplete=lambda self, ctx: [
270 | guild.name for guild in self.client.guilds if not any(guild.id == g for g in config.owner_guild_ids)
271 | ],
272 | )
273 | @check.is_owner()
274 | async def leave_guild(self, ctx: discord.ApplicationContext, guild: discord.Guild):
275 | """Leaves a guild."""
276 | if any(guild.id == g for g in config.owner_guild_ids):
277 | error_em = discord.Embed(
278 | description=f"{emoji.error} I can't leave the owner guild", color=config.color.error
279 | )
280 | await ctx.respond(embed=error_em, ephemeral=True)
281 | else:
282 | await guild.leave()
283 | leave_em = discord.Embed(
284 | title=f"{emoji.minus} Left Guild",
285 | description=f"Left **{guild.name}**",
286 | color=config.color.error,
287 | )
288 | await ctx.respond(embed=leave_em)
289 |
290 | # Guild invite
291 | @guild.command(name="invite")
292 | @option(
293 | "guild",
294 | description="Enter the guild name",
295 | autocomplete=lambda self, ctx: [
296 | guild.name for guild in self.client.guilds if not any(guild.id == g for g in config.owner_guild_ids)
297 | ],
298 | )
299 | @check.is_owner()
300 | async def guild_inv(self, ctx: discord.ApplicationContext, guild: discord.Guild):
301 | """Creates an invite link for the guild."""
302 | if any(guild.id == g for g in config.owner_guild_ids):
303 | error_em = discord.Embed(
304 | description=f"{emoji.error} I can't create an invite link for the owner guild",
305 | color=config.color.error,
306 | )
307 | await ctx.respond(embed=error_em, ephemeral=True)
308 | else:
309 | invite = await guild.text_channels[0].create_invite(max_age=0, max_uses=0)
310 | invite_em = discord.Embed(
311 | title=f"{emoji.plus} Guild Invite Link",
312 | description=f"Invite link for **{guild.name}**: {invite}",
313 | color=config.color.theme,
314 | )
315 | await ctx.respond(embed=invite_em)
316 |
317 | # Emoji slash cmd group
318 | emoji = SlashCommandGroup(guild_ids=config.owner_guild_ids, name="emoji", description="Emoji related commands.")
319 |
320 | # Download app emojis
321 | @emoji.command(name="download")
322 | @check.is_dev()
323 | async def download_app_emojis(self, ctx: discord.ApplicationContext):
324 | """Downloads all emojis from the app."""
325 | await ctx.defer()
326 | emojis: list[discord.AppEmoji] = await self.client.fetch_emojis()
327 | if not emojis:
328 | no_emojis_em = discord.Embed(
329 | description=f"{emoji.error} No emojis found in the app.", color=config.color.error
330 | )
331 | await ctx.respond(embed=no_emojis_em, ephemeral=True)
332 | return
333 |
334 | zip_buffer = BytesIO()
335 | with zipfile.ZipFile(zip_buffer, "w") as zip_file:
336 | for app_emoji in emojis:
337 | async with self.client.http._HTTPClient__session.get(app_emoji.url) as response:
338 | if response.status == 200:
339 | zip_file.writestr(f"{app_emoji.name}.png", await response.read())
340 |
341 | zip_buffer.seek(0)
342 | await ctx.respond(file=discord.File(fp=zip_buffer, filename="emojis.zip"))
343 |
344 | # Upload app emojis
345 | @emoji.command(name="upload")
346 | @option("file", description="Upload emojis zip file", type=discord.Attachment)
347 | @check.is_dev()
348 | async def upload_app_emojis(self, ctx: discord.ApplicationContext, file: discord.Attachment):
349 | """Uploads all emojis to the app. (Only supports .zip files with .png emojis)"""
350 | await ctx.defer()
351 | if not file.filename.endswith(".zip"):
352 | error_em = discord.Embed(
353 | description=f"{emoji.error} Please upload a valid zip file.", color=config.color.error
354 | )
355 | await ctx.respond(embed=error_em, ephemeral=True)
356 | return
357 | zip_buffer = BytesIO()
358 | await file.save(zip_buffer)
359 | zip_buffer.seek(0)
360 | with zipfile.ZipFile(zip_buffer, "r") as zip_file:
361 | emojis = [discord.PartialEmoji(name=name, id=None) for name in zip_file.namelist()]
362 | for _emoji in emojis:
363 | if _emoji.name.endswith(".png"):
364 | _emoji.name = _emoji.name[:-4]
365 | if len(_emoji.name) > 32:
366 | error_em = discord.Embed(
367 | description=f"{_emoji.error} Emoji name `{_emoji.name}` is too long (max 32 characters).",
368 | color=config.color.error,
369 | )
370 | await ctx.respond(embed=error_em, ephemeral=True)
371 | return
372 | try:
373 | await self.client.create_emoji(name=_emoji.name, image=zip_file.read(f"{_emoji.name}.png"))
374 | except Exception:
375 | await self.client.delete_emoji(
376 | [emoji for emoji in await self.client.fetch_emojis() if emoji.name == _emoji.name][0]
377 | )
378 | finally:
379 | await self.client.create_emoji(name=_emoji.name, image=zip_file.read(f"{_emoji.name}.png"))
380 | zip_buffer.close()
381 | upload_em = discord.Embed(
382 | title=f"{emoji.upload} Uploaded Emoji(s)",
383 | description=f"Uploaded {len(emojis)} emojis.",
384 | color=config.color.theme,
385 | )
386 | await ctx.respond(embed=upload_em)
387 |
388 | # Sync app emojis
389 | @emoji.command(name="sync")
390 | @check.is_dev()
391 | async def sync_app_emojis(self, ctx: discord.ApplicationContext):
392 | """Syncs all emojis from the app."""
393 | await ctx.defer()
394 | emojis: list[discord.AppEmoji] = await self.client.fetch_emojis()
395 | emoji_dict: dict = {}
396 | if not emojis:
397 | no_emojis_em = discord.Embed(
398 | description=f"{emoji.error} No emojis found in the app.", color=config.color.error
399 | )
400 | await ctx.respond(embed=no_emojis_em, ephemeral=True)
401 | return
402 |
403 | for app_emoji in emojis:
404 | if app_emoji.animated:
405 | emoji_dict[app_emoji.name] = f""
406 | else:
407 | emoji_dict[app_emoji.name] = f"<:{app_emoji.name}:{app_emoji.id}>"
408 |
409 | resp: dict = Emoji.create_custom_emoji_config(emoji_dict)
410 | if resp["status"] == "error":
411 | error_em = discord.Embed(
412 | description=f"{emoji.error} Missing emojis:\n{'\n'.join([f'{emoji.bullet} `{i}`' for i in resp['missing_keys']])}",
413 | color=config.color.error,
414 | )
415 | await ctx.respond(embed=error_em, ephemeral=True)
416 | else:
417 | sync_em = discord.Embed(
418 | title=f"{emoji.restart} Synced Emoji(s)",
419 | description=f"Synced {len(emojis)} emoji(s).",
420 | color=config.color.theme,
421 | )
422 | if resp.get("extra_keys"):
423 | sync_em.add_field(
424 | name=f"{emoji.error} Extra emoji(s)",
425 | value="\n".join([f"{emoji.bullet} `{i}`: {i}" for i in resp["extra_keys"]]),
426 | )
427 | await ctx.respond(embed=sync_em)
428 |
429 |
430 | def setup(client: discord.Bot):
431 | client.add_cog(Devs(client))
432 |
--------------------------------------------------------------------------------
/cogs/error_handler.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from discord.ext import commands
3 | from utils import check, config
4 | from utils.emoji import emoji
5 | from utils.helpers import fmt_perms
6 |
7 |
8 | class ErrorHandler(commands.Cog):
9 | def __init__(self, client):
10 | self.client = client
11 |
12 | # Slash cmd Error Handler
13 | @commands.Cog.listener()
14 | async def on_application_command_error(self, ctx: discord.ApplicationContext, error: discord.DiscordException):
15 | error_em = discord.Embed(color=config.color.error)
16 |
17 | if isinstance(error, commands.CommandNotFound):
18 | pass
19 |
20 | elif isinstance(error, commands.CommandOnCooldown):
21 | error_em.description = f"{emoji.error} You're on cooldown. Try again in {error.retry_after:.0f} seconds."
22 |
23 | elif isinstance(error, commands.BotMissingPermissions):
24 | error_em.description = (f"{emoji.error} I don't have {fmt_perms(error.missing_permissions)} permission(s)",)
25 |
26 | elif isinstance(error, commands.MissingPermissions):
27 | error_em.description = (
28 | f"{emoji.error} You need {fmt_perms(error.missing_permissions)} permission(s) to use this command."
29 | )
30 |
31 | elif isinstance(error, discord.errors.Forbidden):
32 | error_em.description = f"{emoji.error} I don't have permission to do that."
33 |
34 | elif await check.is_dev().predicate(ctx):
35 | error_em.description = (
36 | f"{emoji.error} An unexpected error occurred: **`{error.__class__.__name__}`**\n```\n{error}```"
37 | )
38 | await ctx.respond(embed=error_em, ephemeral=True)
39 |
40 |
41 | def setup(client: discord.Bot):
42 | client.add_cog(ErrorHandler(client))
43 |
--------------------------------------------------------------------------------
/cogs/help.py:
--------------------------------------------------------------------------------
1 | import discord
2 | import discord.ui
3 | from discord.commands import SlashCommandGroup, slash_command
4 | from discord.ext import commands
5 | from utils import config
6 | from utils.emoji import emoji
7 |
8 |
9 | # Help embed
10 | def help_home_em(self, ctx: discord.ApplicationContext):
11 | help_em = discord.Embed(
12 | title=f"{self.client.user.name} Help Desk",
13 | description=f"Hello {ctx.author.mention}! I'm {self.client.user.name}, use the dropdown menu below to see the commands of each category. If you need help, feel free to ask in the [support server]({config.support_server_url}).",
14 | color=config.color.theme,
15 | )
16 | help_em.add_field(
17 | name="Categories",
18 | value=f"{emoji.mod} `:` **Moderation**\n"
19 | + f"{emoji.mass_mod} `:` **Mass Moderation**\n"
20 | + f"{emoji.info} `:` **Info**\n"
21 | + f"{emoji.settings} `:` **Settings**\n"
22 | + f"{emoji.music} `:` **Music**\n"
23 | + f"{emoji.ticket} `:` **Tickets**",
24 | )
25 | return help_em
26 |
27 |
28 | class HelpView(discord.ui.View):
29 | def __init__(self, client: discord.Bot, ctx: discord.ApplicationContext, timeout: int):
30 | super().__init__(timeout=timeout, disable_on_timeout=True)
31 | self.client = client
32 | self.ctx = ctx
33 |
34 | # Interaction check
35 | async def interaction_check(self, interaction: discord.Interaction):
36 | if interaction.user != self.ctx.author:
37 | help_check_em = discord.Embed(
38 | description=f"{emoji.error} You are not the author of this message", color=config.color.error
39 | )
40 | await interaction.response.send_message(embed=help_check_em, ephemeral=True)
41 | return False
42 | else:
43 | return True
44 |
45 | # Help select menu
46 | @discord.ui.select(
47 | placeholder="Choose A Category",
48 | min_values=1,
49 | max_values=1,
50 | custom_id="help",
51 | options=[
52 | discord.SelectOption(
53 | label="Moderation",
54 | description="Moderate your server & keep it managed by using commands.",
55 | emoji=f"{emoji.mod}",
56 | ),
57 | discord.SelectOption(
58 | label="Mass Moderation", description="Moderate your server in bulk.", emoji=f"{emoji.mass_mod}"
59 | ),
60 | discord.SelectOption(
61 | label="Info", description="See some info about bot and others.", emoji=f"{emoji.info}"
62 | ),
63 | discord.SelectOption(
64 | label="Settings", description="Highly customisable server settings.", emoji=f"{emoji.settings}"
65 | ),
66 | discord.SelectOption(
67 | label="Music", description="Wanna chill? Just play music & enjoy.", emoji=f"{emoji.music}"
68 | ),
69 | discord.SelectOption(
70 | label="Tickets", description="Need help? Create a ticket and ask.", emoji=f"{emoji.ticket}"
71 | ),
72 | discord.SelectOption(label="Home", description="Go back to home.", emoji=f"{emoji.previous}"),
73 | ],
74 | )
75 | async def help_callback(self, select: discord.ui.Select, interaction: discord.Interaction):
76 | cmds = ""
77 | if select.values[0] == "Home":
78 | await interaction.response.edit_message(embed=help_home_em(self, self.ctx))
79 | else:
80 | cog = self.client.get_cog(select.values[0].replace(" ", ""))
81 | for command in cog.get_commands():
82 | if isinstance(command, SlashCommandGroup):
83 | for subcommand in command.walk_commands():
84 | cmds += f"{command.name} {subcommand.name}:{command.id}>\n{emoji.bullet} {subcommand.description}\n\n"
85 | else:
86 | cmds += f"{command.name}:{command.id}>\n{emoji.bullet} {command.description}\n\n"
87 | help_em = discord.Embed(
88 | title=f"{select.values[0]} Commands", description=f"{cmds}", color=config.color.theme
89 | )
90 | await interaction.response.edit_message(embed=help_em)
91 |
92 |
93 | class Help(commands.Cog):
94 | def __init__(self, client):
95 | self.client = client
96 |
97 | # Help
98 | @slash_command(name="help")
99 | async def help(self, ctx: discord.ApplicationContext):
100 | """Need bot's help? Use this!"""
101 | helpView = HelpView(self.client, ctx, timeout=60)
102 | helpView.msg = await ctx.respond(embed=help_home_em(self, ctx), view=helpView)
103 |
104 |
105 | def setup(client: discord.Bot):
106 | client.add_cog(Help(client))
107 |
--------------------------------------------------------------------------------
/cogs/info.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import discord
3 | import platform
4 | import time
5 | from discord.commands import SlashCommandGroup, option, slash_command
6 | from discord.ext import commands
7 | from utils import config
8 | from utils.emoji import emoji
9 |
10 | # Starting time of bot
11 | start_time = time.time()
12 |
13 |
14 | class Info(commands.Cog):
15 | def __init__(self, client):
16 | self.client = client
17 |
18 | # Ping
19 | @slash_command(name="ping")
20 | async def ping(self, ctx: discord.ApplicationContext):
21 | """Shows heartbeats of the bot."""
22 | ping_em = discord.Embed(
23 | description=f"{emoji.bullet} **Ping**: `{round(self.client.latency * 1000)} ms`",
24 | color=config.color.theme,
25 | )
26 | await ctx.respond(embed=ping_em)
27 |
28 | # Uptime
29 | @slash_command(name="uptime")
30 | async def uptime(self, ctx: discord.ApplicationContext):
31 | """Shows bot's uptime."""
32 | uptime_em = discord.Embed(
33 | description=f"{emoji.bullet} **Bot's Uptime**: `{str(datetime.timedelta(seconds=int(round(time.time() - start_time))))}`",
34 | color=config.color.theme,
35 | )
36 | await ctx.respond(embed=uptime_em)
37 |
38 | # Stats
39 | @slash_command(name="stats")
40 | async def stats(self, ctx: discord.ApplicationContext):
41 | """Shows bot stats."""
42 | owner = await self.client.fetch_user(config.owner_id)
43 | stats_em = discord.Embed(
44 | title=f"{self.client.user.name} Stats",
45 | description=f"{emoji.bullet} **Bot's Latency**: `{round(self.client.latency * 1000)} ms`\n"
46 | + f"{emoji.bullet} **Bot's Uptime**: `{str(datetime.timedelta(seconds=int(round(time.time() - start_time))))}`\n"
47 | + f"{emoji.bullet} **Total Servers**: `{str(len(self.client.guilds))}`\n"
48 | + f"{emoji.bullet} **Total Members**: `{len(set(self.client.get_all_members()))}`\n"
49 | + f"{emoji.bullet} **Total Channels**: `{len(set(self.client.get_all_channels()))}`\n"
50 | + f"{emoji.bullet} **Python Version**: `v{platform.python_version()}`\n"
51 | + f"{emoji.bullet} **Pycord Version**: `v{discord.__version__}`",
52 | color=config.color.theme,
53 | )
54 | stats_em.set_footer(text=f"Designed & Built by {owner}", icon_url=f"{owner.avatar.url}")
55 | await ctx.respond(embed=stats_em)
56 |
57 | # Avatar
58 | @slash_command(name="avatar")
59 | @option("user", description="Mention the user whom you will see avatar")
60 | async def avatar(self, ctx: discord.ApplicationContext, user: discord.Member):
61 | """Shows the avatar of the mentioned user."""
62 | avatar_em = discord.Embed(
63 | title=f"{user.name}'s Avatar", description=f"[Avatar URL]({user.avatar.url})", color=config.color.theme
64 | )
65 | avatar_em.set_image(url=f"{user.avatar.url}")
66 | await ctx.respond(embed=avatar_em)
67 |
68 | # Info slash cmd group
69 | info = SlashCommandGroup(name="info", description="Info related commands.")
70 |
71 | # User info
72 | @info.command(name="user")
73 | @option("user", description="Mention the member whom you will see info")
74 | async def user_info(self, ctx: discord.ApplicationContext, user: discord.Member):
75 | """Shows info of the mentioned user."""
76 | user_info_em = discord.Embed(
77 | title=f"{user.name}'s Info",
78 | description=f"{emoji.bullet} **Name**: `{user}`\n"
79 | + f"{emoji.bullet} **ID**: `{user.id}`\n"
80 | + f"{emoji.bullet} **Bot?**: {user.bot}\n"
81 | + f"{emoji.bullet} **Avatar URL**: [Click Here]({user.avatar.url})\n"
82 | + f"{emoji.bullet} **Status**: {user.status}\n"
83 | + f"{emoji.bullet} **Nickname**: {user.nick}\n"
84 | + f"{emoji.bullet} **Highest Role**: {user.top_role.mention}\n"
85 | + f"{emoji.bullet} **Account Created**: {discord.utils.format_dt(user.created_at, 'R')}\n"
86 | + f"{emoji.bullet} **Server Joined**: {discord.utils.format_dt(user.joined_at, 'R')}",
87 | color=config.color.theme,
88 | )
89 | user_info_em.set_thumbnail(url=f"{user.avatar.url}")
90 | await ctx.respond(embed=user_info_em)
91 |
92 | # Server info
93 | @info.command(name="server")
94 | async def server_info(self, ctx: discord.ApplicationContext):
95 | """Shows info of the current server."""
96 | server_info_em = discord.Embed(
97 | title=f"{ctx.guild.name}'s Info",
98 | description=f"{emoji.bullet} **Name**: {ctx.guild.name}\n"
99 | + f"{emoji.bullet} **ID**: `{ctx.guild.id}`\n"
100 | + f"{emoji.bullet} **Icon URL**: {f'[Click Here]({ctx.guild.icon})' if ctx.guild.icon else 'None'}\n"
101 | + f"{emoji.bullet} **Owner**: {ctx.guild.owner.mention}\n"
102 | + f"{emoji.bullet} **Verification Level**: `{ctx.guild.verification_level}`\n"
103 | + f"{emoji.bullet} **Total Categorie(s)**: `{len(ctx.guild.categories)}`\n"
104 | + f"{emoji.bullet} **Total Channel(s)**: `{len(ctx.guild.text_channels) + len(ctx.guild.voice_channels)}`\n"
105 | + f"{emoji.bullet} **Text Channel(s)**: `{len(ctx.guild.text_channels)}`\n"
106 | + f"{emoji.bullet} **Voice Channel(s)**: `{len(ctx.guild.voice_channels)}`\n"
107 | + f"{emoji.bullet} **Stage Channel(s)**: `{len(ctx.guild.stage_channels)}`\n"
108 | + f"{emoji.bullet} **Total Member(s)**: `{len(list(ctx.guild.members))}`\n"
109 | + f"{emoji.bullet} **Human(s)**: `{len([m for m in ctx.guild.members if not m.bot])}`\n"
110 | + f"{emoji.bullet} **Bot(s)**: `{len([m for m in ctx.guild.members if m.bot])}`\n"
111 | + f"{emoji.bullet} **Role(s)**: `{len(ctx.guild.roles)}`\n"
112 | + f"{emoji.bullet} **Server Created**: {discord.utils.format_dt(ctx.guild.created_at, 'R')}",
113 | color=config.color.theme,
114 | )
115 | server_info_em.set_thumbnail(url=ctx.guild.icon if ctx.guild.icon else "")
116 | await ctx.respond(embed=server_info_em)
117 |
118 | # Emoji info
119 | @info.command(name="emoji")
120 | @option("icon", description="Enter the emoji")
121 | async def emoji_info(self, ctx: discord.ApplicationContext, icon: discord.Emoji):
122 | """Shows info of the given emoji."""
123 | emoji_info_em = discord.Embed(
124 | description=f"{emoji.bullet} **Name**: {icon.name}\n"
125 | + f"{emoji.bullet} **ID**: `{icon.id}`\n"
126 | + f"{emoji.bullet} **Emoji URL**: [Click Here]({icon.url})\n"
127 | + f"{emoji.bullet} **Is Animated?**: {icon.animated}\n"
128 | + f"{emoji.bullet} **Usage**: `{icon}`\n"
129 | + f"{emoji.bullet} **Emoji Created**: {discord.utils.format_dt(icon.created_at, 'R')}",
130 | color=config.color.theme,
131 | )
132 | emoji_info_em.set_thumbnail(url=f"{icon.url}")
133 | await ctx.respond(embed=emoji_info_em)
134 |
135 |
136 | def setup(client: discord.Bot):
137 | client.add_cog(Info(client))
138 |
--------------------------------------------------------------------------------
/cogs/mass_moderation.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import discord
3 | from babel.dates import format_timedelta
4 | from db.funcs.guild import fetch_guild_settings
5 | from discord.commands import SlashCommandGroup, option
6 | from discord.ext import commands
7 | from utils import config
8 | from utils.emoji import emoji
9 | from utils.helpers import parse_duration
10 |
11 |
12 | class MassModeration(commands.Cog):
13 | def __init__(self, client):
14 | self.client = client
15 |
16 | # Mass slash cmd group
17 | mass = SlashCommandGroup(
18 | name="mass",
19 | description="Mass moderation commands.",
20 | default_member_permissions=discord.Permissions(manage_guild=True),
21 | )
22 |
23 | # Mass kick
24 | @mass.command(name="kick")
25 | @option("users", description='Mention the users whom you want to kick. Use "," to separate users.', required=True)
26 | @option("reason", description="Enter the reason for kicking the user", required=False)
27 | async def mass_kick_users(self, ctx: discord.ApplicationContext, users: str, reason: str = None):
28 | """Kicks mentioned users."""
29 | await ctx.defer()
30 | users: list = users.split(",")
31 | _users: list = []
32 | errors: list[tuple] = []
33 | if len(users) > 10:
34 | error_em = discord.Embed(
35 | description=f"{emoji.error} You can only mass kick upto 10 users.", color=config.color.error
36 | )
37 | await ctx.respond(embed=error_em, ephemeral=True)
38 | else:
39 | for user in users:
40 | try:
41 | _user = await commands.MemberConverter().convert(ctx, user.strip())
42 | except Exception:
43 | errors.append((user.strip(), "User not found."))
44 | continue
45 | if _user == ctx.author:
46 | errors.append((ctx.author.mention, "You cannot use it on yourself."))
47 | continue
48 | elif _user.top_role.position >= ctx.author.top_role.position:
49 | errors.append((_user.mention, "User has same role or higher role than you."))
50 | continue
51 | else:
52 | _users.append(_user.mention)
53 | await _user.kick(reason=reason)
54 | if len(_users) > 0:
55 | mass_kick_em = discord.Embed(
56 | title=f"{emoji.kick} Mass Kicked Users",
57 | description=f"Successfully kicked {len(_users)} users.\n"
58 | + f"{emoji.bullet2} **Reason**: {reason}\n"
59 | + f"{emoji.bullet2} **Users**: {', '.join(_users)}",
60 | color=config.color.error,
61 | )
62 | await ctx.respond(embed=mass_kick_em)
63 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
64 | if channel_id:
65 | log_ch = await self.client.fetch_channel(channel_id)
66 | mass_kick_em.description += f"\n{emoji.bullet2} **Moderator**: {ctx.author.mention}"
67 | await log_ch.send(embed=mass_kick_em)
68 | if len(errors) > 0:
69 | error_em = discord.Embed(
70 | title=f"{emoji.error} Can't kick users",
71 | description="\n".join([f"{emoji.bullet2} **{user}**: {reason}" for user, reason in errors]),
72 | color=config.color.error,
73 | )
74 | await ctx.respond(embed=error_em, ephemeral=True)
75 |
76 | # Mass ban
77 | @mass.command(name="ban")
78 | @option("users", description='Mention the users whom you want to ban. Use "," to separate users.', required=True)
79 | @option("reason", description="Enter the reason for banning the user", required=False)
80 | async def mass_ban_users(self, ctx: discord.ApplicationContext, users: str, reason: str = None):
81 | """Bans mentioned users."""
82 | await ctx.defer()
83 | users: list = users.split(",")
84 | _users: list = []
85 | errors: list[tuple] = []
86 | if len(users) > 10:
87 | error_em = discord.Embed(
88 | description=f"{emoji.error} You can only mass ban upto 10 users.", color=config.color.error
89 | )
90 | await ctx.respond(embed=error_em, ephemeral=True)
91 | else:
92 | for user in users:
93 | try:
94 | _user = await commands.MemberConverter().convert(ctx, user.strip())
95 | except Exception:
96 | errors.append((user.strip(), "User not found."))
97 | continue
98 | if _user == ctx.author:
99 | errors.append((ctx.author.mention, "You cannot use it on yourself."))
100 | continue
101 | elif _user.top_role.position >= ctx.author.top_role.position:
102 | errors.append((_user.mention, "User has same role or higher role than you."))
103 | continue
104 | _users.append(_user.mention)
105 | await _user.ban(reason=reason)
106 | if len(_users) > 0:
107 | mass_ban_em = discord.Embed(
108 | title=f"{emoji.mod2} Mass Banned Users",
109 | description=f"Successfully banned {len(_users)} users.\n"
110 | + f"{emoji.bullet2} **Reason**: {reason}\n"
111 | + f"{emoji.bullet2} **Users**: {', '.join(_users)}",
112 | color=config.color.error,
113 | )
114 | await ctx.respond(embed=mass_ban_em)
115 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
116 | if channel_id:
117 | log_ch = await self.client.fetch_channel(channel_id)
118 | mass_ban_em.description += f"\n{emoji.bullet2} **Moderator**: {ctx.author.mention}"
119 | await log_ch.send(embed=mass_ban_em)
120 | if len(errors) > 0:
121 | error_em = discord.Embed(
122 | title=f"{emoji.error} Can't ban users",
123 | description="\n".join([f"{emoji.bullet2} **{user}**: {reason}" for user, reason in errors]),
124 | color=config.color.error,
125 | )
126 | await ctx.respond(embed=error_em, ephemeral=True)
127 |
128 | # Mass timeout users
129 | @mass.command(name="timeout")
130 | @option(
131 | "users", description='Mention the users whom you want to timeout. Use "," to separate users.', required=True
132 | )
133 | @option("duration", description="Enter the duration of timeout. Ex: 1d, 2w etc...")
134 | @option("reason", description="Enter the reason for user timeout", required=False)
135 | async def mass_timeout_users(self, ctx: discord.ApplicationContext, users: str, duration: str, reason: str = None):
136 | """Timeouts mentioned users."""
137 | await ctx.defer()
138 | users: list = users.split(",")
139 | _users: list = []
140 | try:
141 | dur: datetime.timedelta = parse_duration(duration)
142 | except ValueError as e:
143 | error_em = discord.Embed(description=f"{emoji.error} {e}", color=config.color.error)
144 | await ctx.respond(embed=error_em, ephemeral=True)
145 | return
146 | errors: list[tuple] = []
147 | if len(users) > 10:
148 | error_em = discord.Embed(
149 | description=f"{emoji.error} You can only mass timeout upto 10 users.", color=config.color.error
150 | )
151 | await ctx.respond(embed=error_em, ephemeral=True)
152 | else:
153 | for user in users:
154 | try:
155 | _user = await commands.MemberConverter().convert(ctx, user.strip())
156 | except Exception:
157 | errors.append((user.strip(), "User not found."))
158 | continue
159 | if _user == ctx.guild.owner:
160 | errors.append((_user.mention, "You cannot use it on server owner."))
161 | continue
162 | elif _user == ctx.author:
163 | errors.append((ctx.author.mention, "You cannot use it on yourself."))
164 | continue
165 | elif _user.top_role.position >= ctx.author.top_role.position:
166 | errors.append((_user.mention, "User has same role or higher role than you."))
167 | continue
168 | else:
169 | _users.append(_user.mention)
170 | await _user.timeout_for(dur, reason=reason)
171 | if len(_users) > 0:
172 | mass_timeout_em = discord.Embed(
173 | title=f"{emoji.timer2} Mass Timed out Users",
174 | description=f"Successfully timed out {len(_users)} users.\n"
175 | + f"{emoji.bullet2} **Duration**: `{format_timedelta(dur, locale='en_IN')}`\n"
176 | + f"{emoji.bullet2} **Reason**: {reason}\n"
177 | + f"{emoji.bullet2} **Users**: {', '.join(_users)}",
178 | color=config.color.error,
179 | )
180 | await ctx.respond(embed=mass_timeout_em)
181 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
182 | if channel_id:
183 | log_ch = await self.client.fetch_channel(channel_id)
184 | mass_timeout_em.description += f"\n{emoji.bullet} **Moderator**: {ctx.author.mention}"
185 | await log_ch.send(embed=mass_timeout_em)
186 | if len(errors) > 0:
187 | error_em = discord.Embed(
188 | title=f"{emoji.error} Can't timeout users",
189 | description="\n".join([f"{emoji.bullet2} **{user}**: {reason}" for user, reason in errors]),
190 | color=config.color.error,
191 | )
192 | await ctx.respond(embed=error_em, ephemeral=True)
193 |
194 | # Mass untimeout users
195 | @mass.command(name="untimeout")
196 | @option(
197 | "users", description='Mention the users whom you want to untimeout. Use "," to separate users.', required=True
198 | )
199 | @option("reason", description="Enter the reason for user timeout", required=False)
200 | async def mass_untimeout_users(self, ctx: discord.ApplicationContext, users: str, reason: str = None):
201 | """Untimeouts mentioned users."""
202 | await ctx.defer()
203 | users: list = users.split(",")
204 | _users: list = []
205 | errors: list[tuple] = []
206 | if len(users) > 10:
207 | error_em = discord.Embed(
208 | description=f"{emoji.error} You can only mass untimeout upto 10 users.", color=config.color.error
209 | )
210 | await ctx.respond(embed=error_em, ephemeral=True)
211 | else:
212 | for user in users:
213 | try:
214 | _user = await commands.MemberConverter().convert(ctx, user.strip())
215 | except Exception:
216 | errors.append((user.strip(), "User not found."))
217 | continue
218 | if _user == ctx.author:
219 | errors.append((ctx.author.mention, "You cannot use it on yourself."))
220 | continue
221 | elif _user.top_role.position >= ctx.author.top_role.position:
222 | errors.append((_user.mention, "User has same role or higher role than you."))
223 | continue
224 | else:
225 | _users.append(_user.mention)
226 | await _user.timeout(None, reason=reason)
227 | if len(_users) > 0:
228 | mass_untimeout_em = discord.Embed(
229 | title=f"{emoji.timer} Mass Untimed out Users",
230 | description=f"Successfully untimed out {len(_users)} users.\n"
231 | + f"{emoji.bullet} **Reason**: {reason}\n"
232 | + f"{emoji.bullet} **Users**: {', '.join(_users)}",
233 | color=config.color.theme,
234 | )
235 | await ctx.respond(embed=mass_untimeout_em)
236 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
237 | if channel_id:
238 | log_ch = await self.client.fetch_channel(channel_id)
239 | mass_untimeout_em.description += f"\n{emoji.bullet} **Moderator**: {ctx.author.mention}"
240 | await log_ch.send(embed=mass_untimeout_em)
241 | if len(errors) > 0:
242 | error_em = discord.Embed(
243 | title=f"{emoji.error} Can't untimeout users",
244 | description="\n".join([f"{emoji.bullet} **{user}**: {reason}" for user, reason in errors]),
245 | color=config.color.error,
246 | )
247 | await ctx.respond(embed=error_em, ephemeral=True)
248 |
249 | # Mass role add
250 | @mass.command(name="role-add")
251 | @option(
252 | "users",
253 | description='Mention the users whom you want to add the role. Use "," to separate users.',
254 | required=True,
255 | )
256 | @option(
257 | "roles",
258 | description='Mention the roles which you will add to the users. Use "," to separate roles.',
259 | required=True,
260 | )
261 | async def mass_role_add(self, ctx: discord.ApplicationContext, users: str, roles: str):
262 | """Adds mentioned roles to mentioned users."""
263 | await ctx.defer()
264 | users: list = users.split(",")
265 | roles: list = roles.split(",")
266 | _users: list = []
267 | _roles: list = []
268 | role_errors: list[tuple] = []
269 | user_errors: list[tuple] = []
270 | if len(users) > 10 or len(roles) > 10:
271 | error_em = discord.Embed(
272 | description=f"{emoji.error} You can only mass add 10 roles upto 10 users.", color=config.color.error
273 | )
274 | await ctx.respond(embed=error_em, ephemeral=True)
275 | else:
276 | for role in roles: # Check roles
277 | try:
278 | _role = await commands.RoleConverter().convert(ctx, role.strip())
279 | except Exception:
280 | role_errors.append((role.strip(), "Role not found."))
281 | continue
282 | if _role.position >= ctx.guild.get_member(self.client.user.id).top_role.position:
283 | role_errors.append((_role.mention, "Role has same or higher position than me."))
284 | continue
285 | else:
286 | _roles.append(_role)
287 | if len(_roles) > 0:
288 | for user in users: # Check users
289 | try:
290 | _user = await commands.MemberConverter().convert(ctx, user.strip())
291 | except Exception:
292 | user_errors.append((user.strip(), "User not found."))
293 | continue
294 | if _user.top_role.position >= ctx.author.top_role.position:
295 | user_errors.append((_user.mention, "User has same role or higher role than you."))
296 | continue
297 | else:
298 | _users.append(_user.mention)
299 | for role in _roles:
300 | await _user.add_roles(role)
301 | if len(_users) > 0 and len(_roles) > 0:
302 | mass_role_add_em = discord.Embed(
303 | title=f"{emoji.plus} Mass Added Roles",
304 | description=f"Successfully added {len(_roles)} roles to {len(_users)} users.\n"
305 | + f"{emoji.bullet} **User(s)**: {', '.join(_users)}\n"
306 | + f"{emoji.bullet} **Role(s)**: {', '.join([role.mention for role in _roles])}",
307 | color=config.color.theme,
308 | )
309 | await ctx.respond(embed=mass_role_add_em)
310 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
311 | if channel_id:
312 | log_ch = await self.client.fetch_channel(channel_id)
313 | mass_role_add_em.description += f"\n{emoji.bullet} **Moderator**: {ctx.author.mention}"
314 | await log_ch.send(embed=mass_role_add_em)
315 | if len(role_errors) > 0 or len(user_errors) > 0:
316 | error_em = discord.Embed(
317 | title=f"{emoji.error} Can't add roles",
318 | description="\n".join(
319 | [f"{emoji.bullet} **{obj}**: {reason}" for obj, reason in role_errors + user_errors]
320 | ),
321 | color=config.color.error,
322 | )
323 | await ctx.respond(embed=error_em, ephemeral=True)
324 |
325 | # Mass role remove
326 | @mass.command(name="role-remove")
327 | @option(
328 | "users",
329 | description='Mention the users whom you want to remove the role. Use "," to separate users.',
330 | required=True,
331 | )
332 | @option(
333 | "roles",
334 | description='Mention the roles which you will remove from the users. Use "," to separate roles.',
335 | required=True,
336 | )
337 | async def mass_role_remove(self, ctx: discord.ApplicationContext, users: str, roles: str):
338 | """Removes mentioned roles from mentioned users."""
339 | await ctx.defer()
340 | users: list = users.split(",")
341 | roles: list = roles.split(",")
342 | _users: list = []
343 | _roles: list = []
344 | role_errors: list[tuple] = []
345 | user_errors: list[tuple] = []
346 | if len(users) > 10 or len(roles) > 10:
347 | error_em = discord.Embed(
348 | description=f"{emoji.error} You can only mass remove 10 roles upto 10 users.",
349 | color=config.color.error,
350 | )
351 | await ctx.respond(embed=error_em, ephemeral=True)
352 | else:
353 | for role in roles: # Check roles
354 | try:
355 | _role = await commands.RoleConverter().convert(ctx, role.strip())
356 | except Exception:
357 | role_errors.append((role.strip(), "Role not found."))
358 | continue
359 | if _role.position >= ctx.guild.get_member(self.client.user.id).top_role.position:
360 | role_errors.append((_role.mention, "Role has same or higher position than me."))
361 | continue
362 | else:
363 | _roles.append(_role)
364 | if len(_roles) > 0:
365 | for user in users: # Check users
366 | try:
367 | _user = await commands.MemberConverter().convert(ctx, user.strip())
368 | except Exception:
369 | user_errors.append((user.strip(), "User not found."))
370 | continue
371 | if _user.top_role.position >= ctx.author.top_role.position:
372 | user_errors.append((_user.mention, "User has same role or higher role than you."))
373 | continue
374 | else:
375 | _users.append(_user.mention)
376 | for role in _roles:
377 | await _user.remove_roles(role)
378 | if len(_users) > 0 and len(_roles) > 0:
379 | mass_role_remove_em = discord.Embed(
380 | title=f"{emoji.minus} Mass Removed Roles",
381 | description=f"Successfully removed {len(_roles)} roles from {len(_users)} users.\n"
382 | + f"{emoji.bullet} **User(s)**: {', '.join(_users)}\n"
383 | + f"{emoji.bullet} **Role(s)**: {', '.join([role.mention for role in _roles])}",
384 | color=config.color.theme,
385 | )
386 | await ctx.respond(embed=mass_role_remove_em)
387 | channel_id = (await fetch_guild_settings(ctx.guild.id)).mod_cmd_log_channel_id
388 | if channel_id:
389 | log_ch = await self.client.fetch_channel(channel_id)
390 | mass_role_remove_em.description += f"\n{emoji.bullet} **Moderator**: {ctx.author.mention}"
391 | await log_ch.send(embed=mass_role_remove_em)
392 | if len(role_errors) > 0 or len(user_errors) > 0:
393 | error_em = discord.Embed(
394 | title=f"{emoji.error} Can't remove roles",
395 | description="\n".join(
396 | [f"{emoji.bullet} **{obj}**: {reason}" for obj, reason in role_errors + user_errors]
397 | ),
398 | color=config.color.error,
399 | )
400 | await ctx.respond(embed=error_em, ephemeral=True)
401 |
402 |
403 | def setup(client: discord.Bot):
404 | client.add_cog(MassModeration(client))
405 |
--------------------------------------------------------------------------------
/cogs/mod_logs.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from db.funcs.guild import fetch_guild_settings
3 | from discord.ext import commands
4 | from utils import config
5 | from utils.emoji import emoji
6 |
7 |
8 | class Logs(commands.Cog):
9 | def __init__(self, client):
10 | self.client = client
11 |
12 | # Join
13 | @commands.Cog.listener()
14 | async def on_member_join(self, user: discord.Member):
15 | channel_id = (await fetch_guild_settings(user.guild.id)).mod_log_channel_id
16 | if channel_id is not None:
17 | join_ch = await self.client.fetch_channel(channel_id)
18 | join_em = discord.Embed(
19 | title=f"{emoji.plus} Member Joined",
20 | description=f"{emoji.bullet} **Name**: {user.mention}\n"
21 | + f"{emoji.bullet} **Account Created**: {discord.utils.format_dt(user.created_at, 'R')}",
22 | color=config.color.theme,
23 | )
24 | join_em.set_thumbnail(url=f"{user.avatar.url}")
25 | await join_ch.send(embed=join_em)
26 |
27 | # Leave
28 | @commands.Cog.listener()
29 | async def on_member_remove(self, user: discord.Member):
30 | channel_id = (await fetch_guild_settings(user.guild.id)).mod_log_channel_id
31 | if channel_id is not None:
32 | leave_ch = await self.client.fetch_channel(channel_id)
33 | leave_em = discord.Embed(
34 | title=f"{emoji.minus} Member Left",
35 | description=f"{emoji.bullet2} **Name**: {user.mention}\n"
36 | + f"{emoji.bullet2} **Account Created**: {discord.utils.format_dt(user.created_at, 'R')}\n"
37 | + f"{emoji.bullet2} **Server Joined**: {discord.utils.format_dt(user.joined_at, 'R')}",
38 | color=config.color.error,
39 | )
40 | leave_em.set_thumbnail(url=f"{user.avatar.url}")
41 | await leave_ch.send(embed=leave_em)
42 |
43 | # Ban
44 | @commands.Cog.listener()
45 | async def on_member_ban(self, user: discord.Member):
46 | channel_id = (await fetch_guild_settings(user.guild.id)).mod_log_channel_id
47 | if channel_id is not None:
48 | ban_ch = await self.client.fetch_channel(channel_id)
49 | ban_em = discord.Embed(
50 | title=f"{emoji.mod2} Member Banned",
51 | description=f"{emoji.bullet2} **Name**: {user.mention}\n"
52 | + f"{emoji.bullet2} **Account Created**: {discord.utils.format_dt(user.created_at, 'R')}\n"
53 | + f"{emoji.bullet2} **Server Joined**: {discord.utils.format_dt(user.joined_at, 'R')}",
54 | color=config.color.error,
55 | )
56 | ban_em.set_thumbnail(url=f"{user.avatar.url}")
57 | await ban_ch.send(embed=ban_em)
58 |
59 | # Unban
60 | @commands.Cog.listener()
61 | async def on_member_unban(self, user: discord.Member):
62 | channel_id = (await fetch_guild_settings(user.guild.id)).mod_log_channel_id
63 | if channel_id is not None:
64 | unban_ch = await self.client.fetch_channel(channel_id)
65 | unban_em = discord.Embed(
66 | title=f"{emoji.mod} Member Unbanned",
67 | description=f"{emoji.bullet} **Name**: {user.mention}\n"
68 | + f"{emoji.bullet} **Account Created**: {discord.utils.format_dt(user.created_at, 'R')}",
69 | color=config.color.theme,
70 | )
71 | unban_em.set_thumbnail(url=f"{user.avatar.url}")
72 | await unban_ch.send(embed=unban_em)
73 |
74 | # Edit
75 | @commands.Cog.listener()
76 | async def on_message_edit(self, msg_before: discord.Message, msg_after: discord.Message):
77 | if msg_before.guild.id:
78 | channel_id = (await fetch_guild_settings(msg_before.guild.id)).mod_log_channel_id
79 | if msg_before.author and msg_after.author == self.client.user:
80 | return
81 | elif msg_before.author.bot:
82 | return
83 | elif channel_id is not None:
84 | edit_ch = await self.client.fetch_channel(channel_id)
85 | edit_em = discord.Embed(
86 | title=f"{emoji.edit} Message Edited",
87 | description=f"{emoji.bullet} **Author**: {msg_before.author.mention}\n"
88 | + f"{emoji.bullet} **Channel**: {msg_before.channel.mention}\n"
89 | + f"{emoji.bullet} **Message:** [Jump to Message]({msg_before.jump_url})\n"
90 | + f"{emoji.bullet2} **Original Message**: {msg_before.content}\n"
91 | + f"{emoji.bullet} **Edited Message**: {msg_after.content}",
92 | color=config.color.theme,
93 | )
94 | if msg_before.attachments:
95 | edit_em.description += (
96 | f"\n{emoji.bullet} **Removed Attachment**: [Click Here]({msg_before.attachments[0].url})"
97 | )
98 | edit_em.set_image(url=msg_before.attachments[0].url)
99 | await edit_ch.send(embed=edit_em)
100 |
101 | # Delete
102 | @commands.Cog.listener()
103 | async def on_message_delete(self, msg: discord.Message):
104 | if msg.guild:
105 | channel_id = (await fetch_guild_settings(msg.guild.id)).mod_log_channel_id
106 | if msg.author == self.client.user:
107 | return
108 | elif msg.author.bot:
109 | return
110 | elif channel_id is not None:
111 | del_ch = await self.client.fetch_channel(channel_id)
112 | del_em = discord.Embed(
113 | title=f"{emoji.bin} Message Deleted",
114 | description=f"{emoji.bullet2} **Author**: {msg.author.mention}\n"
115 | + f"{emoji.bullet2} **Channel**: {msg.channel.mention}\n"
116 | + f"{emoji.bullet2} **Message**: {msg.content}",
117 | color=config.color.error,
118 | )
119 | if msg.attachments:
120 | del_em.description += f"\n{emoji.bullet2} **Attachment(s)**: {', '.join([f'[Click Here]({attachment.url})' for attachment in msg.attachments])}"
121 | await del_ch.send(embed=del_em)
122 | # Deleted Attachments
123 | if msg.attachments:
124 | del_list: list = []
125 | for attachment in msg.attachments:
126 | del_em = discord.Embed(
127 | title=f"{emoji.bin} Attachment Deleted",
128 | description=f"{emoji.bullet2} **Author**: {msg.author.mention}\n"
129 | + f"{emoji.bullet2} **Channel**: {msg.channel.mention}\n"
130 | + f"{emoji.bullet2} **Attachment**: [Click Here]({attachment.url})",
131 | color=config.color.error,
132 | )
133 | del_em.set_image(url=attachment.url)
134 | del_list.append(del_em)
135 | await del_ch.send(
136 | embeds=del_list
137 | ) # Limited to send 10 embeds because of discord limitations, user: discord.Members are also limited to 10 attachments per message so this will work fine.
138 |
139 | # Bulk delete
140 | @commands.Cog.listener()
141 | async def on_bulk_message_delete(self, msgs: list[discord.Message]):
142 | if msgs[0].guild:
143 | channel_id = (await fetch_guild_settings(msgs[0].guild.id)).mod_log_channel_id
144 | if msgs[0].author == self.client.user:
145 | return
146 | elif msgs[0].author.bot:
147 | return
148 | elif channel_id is not None:
149 | bulk_ch = await self.client.fetch_channel(channel_id)
150 | bulk_em = discord.Embed(
151 | title=f"{emoji.bin} Bulk Message Deleted",
152 | description=f"{emoji.bullet2} **Author**: {msgs[0].author.mention}\n"
153 | + f"{emoji.bullet2} **Channel**: {msgs[0].channel.mention}\n"
154 | + f"{emoji.bullet2} **Messages Deleted**: {len(msgs)}",
155 | color=config.color.error,
156 | )
157 | await bulk_ch.send(embed=bulk_em)
158 |
159 |
160 | def setup(client: discord.Bot):
161 | client.add_cog(Logs(client))
162 |
--------------------------------------------------------------------------------
/cogs/moderation.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import discord
3 | from babel.dates import format_timedelta
4 | from discord.commands import SlashCommandGroup, option, slash_command
5 | from discord.ext import commands
6 | from utils import config
7 | from utils.emoji import emoji
8 | from utils.helpers import parse_duration
9 |
10 |
11 | class Moderation(commands.Cog):
12 | def __init__(self, client):
13 | self.client = client
14 |
15 | # Purge slash cmd group
16 | purge = SlashCommandGroup(
17 | name="purge",
18 | description="Purge related commands.",
19 | default_member_permissions=discord.Permissions(manage_messages=True),
20 | )
21 |
22 | # Purge any
23 | @purge.command(name="any")
24 | @option("amount", description="Enter an integer between 1 to 1000.")
25 | async def purge_any(self, ctx: discord.ApplicationContext, amount: int):
26 | """Purges the amount of given messages."""
27 | amount_condition = [amount < 1, amount > 1000]
28 | await ctx.defer(ephemeral=True)
29 | if any(amount_condition):
30 | error_em = discord.Embed(
31 | description=f"{emoji.error} Amount must be between 1 to 1000.", color=config.color.error
32 | )
33 | await ctx.respond(embed=error_em, ephemeral=True)
34 | else:
35 | await ctx.channel.purge(limit=amount)
36 | purge_em = discord.Embed(
37 | title=f"{emoji.bin} Messages Purged",
38 | description=f"Successfully purged `{amount}` message(s)",
39 | color=config.color.theme,
40 | )
41 | await ctx.respond(embed=purge_em, ephemeral=True)
42 |
43 | # Purge humans
44 | @purge.command(name="humans")
45 | @option("amount", description="Enter an integer between 1 to 1000.")
46 | async def purge_humans(self, ctx: discord.ApplicationContext, amount: int):
47 | """Purges the amount of given messages sent by humans."""
48 | amount_condition = [amount < 1, amount > 1000]
49 | await ctx.defer(ephemeral=True)
50 | if any(amount_condition):
51 | error_em = discord.Embed(
52 | description=f"{emoji.error} Amount must be between 1 to 1000.", color=config.color.error
53 | )
54 | await ctx.respond(embed=error_em, ephemeral=True)
55 | else:
56 | await ctx.channel.purge(limit=amount, check=lambda m: not m.author.bot)
57 | purge_em = discord.Embed(
58 | title=f"{emoji.bin} Messages Purged",
59 | description=f"Successfully purged `{amount}` message(s) sent by humans",
60 | color=config.color.theme,
61 | )
62 | await ctx.respond(embed=purge_em, ephemeral=True)
63 |
64 | # Purge bots
65 | @purge.command(name="bots")
66 | @option("amount", description="Enter an integer between 1 to 1000.")
67 | async def purge_bots(self, ctx: discord.ApplicationContext, amount: int):
68 | """Purges the amount of given messages sent by bots."""
69 | amount_condition = [amount < 1, amount > 1000]
70 | await ctx.defer(ephemeral=True)
71 | if any(amount_condition):
72 | error_em = discord.Embed(
73 | description=f"{emoji.error} Amount must be between 1 to 1000.", color=config.color.error
74 | )
75 | await ctx.respond(embed=error_em, ephemeral=True)
76 | else:
77 | await ctx.channel.purge(limit=amount, check=lambda m: m.author.bot)
78 | purge_em = discord.Embed(
79 | title=f"{emoji.bin} Messages Purged",
80 | description=f"Successfully purged `{amount}` message(s) sent by bots",
81 | color=config.color.theme,
82 | )
83 | await ctx.respond(embed=purge_em, ephemeral=True)
84 |
85 | # Purge user
86 | @purge.command(name="user")
87 | @option("amount", description="Enter an integer between 1 to 1000.")
88 | @option("user", description="Mention the user whose messages you want to purge.")
89 | async def purge_user(self, ctx: discord.ApplicationContext, amount: int, user: discord.Member):
90 | """Purges the amount of given messages sent by the mentioned user."""
91 | amount_condition = [amount < 1, amount > 1000]
92 | await ctx.defer(ephemeral=True)
93 | if any(amount_condition):
94 | error_em = discord.Embed(
95 | description=f"{emoji.error} Amount must be between 1 to 1000.", color=config.color.error
96 | )
97 | await ctx.respond(embed=error_em, ephemeral=True)
98 | else:
99 | await ctx.channel.purge(limit=amount, check=lambda m: m.author.id == user.id)
100 | purge_em = discord.Embed(
101 | title=f"{emoji.bin} Messages Purged",
102 | description=f"Successfully purged `{amount}` message(s) sent by {user.mention}",
103 | color=config.color.theme,
104 | )
105 | await ctx.respond(embed=purge_em, ephemeral=True)
106 |
107 | # Purge containing phrase
108 | @purge.command(name="contains")
109 | @option("amount", description="Enter an integer between 1 to 1000.")
110 | @option("phrase", description="Enter the phrase to purge messages containing it.")
111 | async def purge_contains(self, ctx: discord.ApplicationContext, amount: int, phrase: str):
112 | """Purges the amount of given messages containing the given phrase."""
113 | amount_condition = [amount < 1, amount > 1000]
114 | await ctx.defer(ephemeral=True)
115 | if any(amount_condition):
116 | error_em = discord.Embed(
117 | description=f"{emoji.error} Amount must be between 1 to 1000.", color=config.color.error
118 | )
119 | await ctx.respond(embed=error_em, ephemeral=True)
120 | else:
121 | await ctx.channel.purge(limit=amount, check=lambda m: phrase.lower() in m.content.lower())
122 | purge_em = discord.Embed(
123 | title=f"{emoji.bin} Messages Purged",
124 | description=f"Successfully purged `{amount}` message(s) containing `{phrase}`",
125 | color=config.color.theme,
126 | )
127 | await ctx.respond(embed=purge_em, ephemeral=True)
128 |
129 | # Kick
130 | @slash_command(name="kick")
131 | @discord.default_permissions(kick_members=True)
132 | @option("user", description="Mention the user whom you want to kick")
133 | @option("reason", description="Enter the reason for kicking the user", required=False)
134 | async def kick(self, ctx: discord.ApplicationContext, user: discord.Member, reason: str = None):
135 | """Kicks the mentioned user."""
136 | if user == ctx.author:
137 | error_em = discord.Embed(
138 | description=f"{emoji.error} You cannot use it on yourself", color=config.color.error
139 | )
140 | await ctx.respond(embed=error_em, ephemeral=True)
141 | elif user.top_role.position >= ctx.author.top_role.position:
142 | error_em = discord.Embed(
143 | description=f"{emoji.error} Given user has same role or higher role than you",
144 | color=config.color.error,
145 | )
146 | await ctx.respond(embed=error_em, ephemeral=True)
147 | else:
148 | kich_em = discord.Embed(
149 | title=f"{emoji.kick} Kicked User",
150 | description=f"Successfully kicked **{user}** from the server.\n"
151 | + f"{emoji.bullet2} **Reason**: {reason}",
152 | color=config.color.error,
153 | )
154 | await user.kick(reason=reason)
155 | await ctx.respond(embed=kich_em)
156 |
157 | # Ban
158 | @slash_command(name="ban")
159 | @discord.default_permissions(ban_members=True)
160 | @option("user", description="Mention the user whom you want to ban")
161 | @option("reason", description="Enter the reason for banning the user", required=False)
162 | async def ban(self, ctx: discord.ApplicationContext, user: discord.Member, reason: str = None):
163 | """Bans the mentioned user."""
164 | if user == ctx.author:
165 | error_em = discord.Embed(
166 | description=f"{emoji.error} You cannot use it on yourself", color=config.color.error
167 | )
168 | await ctx.respond(embed=error_em, ephemeral=True)
169 | elif user.top_role.position >= ctx.author.top_role.position:
170 | error_em = discord.Embed(
171 | description=f"{emoji.error} Given user has same role or higher role than you",
172 | color=config.color.error,
173 | )
174 | await ctx.respond(embed=error_em, ephemeral=True)
175 | else:
176 | ban_em = discord.Embed(
177 | title=f"{emoji.mod2} Banned User",
178 | description=f"Successfully banned **{user}** from the server.\n"
179 | + f"{emoji.bullet2} **Reason**: {reason}",
180 | color=config.color.error,
181 | )
182 | await user.ban(reason=reason)
183 | await ctx.respond(embed=ban_em)
184 |
185 | # Timeout user
186 | @slash_command(name="timeout")
187 | @discord.default_permissions(moderate_members=True)
188 | @option("user", description="Mention the user whom you want to timeout")
189 | @option("duration", description="Enter the duration of timeout. Ex: 1d, 2w etc...")
190 | @option("reason", description="Enter the reason for user timeout", required=False)
191 | async def timeout_user(
192 | self, ctx: discord.ApplicationContext, user: discord.Member, duration: str, reason: str = None
193 | ):
194 | """Timeouts the mentioned user."""
195 | if user == ctx.author:
196 | error_em = discord.Embed(
197 | description=f"{emoji.error} You cannot use it on yourself.", color=config.color.error
198 | )
199 | await ctx.respond(embed=error_em, ephemeral=True)
200 | elif user.top_role.position >= ctx.author.top_role.position:
201 | error_em = discord.Embed(
202 | description=f"{emoji.error} Given user has same role or higher role than you.",
203 | color=config.color.error,
204 | )
205 | await ctx.respond(embed=error_em, ephemeral=True)
206 | else:
207 | try:
208 | dur: datetime.timedelta = parse_duration(duration)
209 | except ValueError as e:
210 | error_em = discord.Embed(description=f"{emoji.error} {e}", color=config.color.error)
211 | await ctx.respond(embed=error_em, ephemeral=True)
212 | return
213 | await user.timeout_for(dur, reason=reason)
214 | timeout_em = discord.Embed(
215 | title=f"{emoji.timer2} Timed out User",
216 | description=f"Successfully timed out {user.mention}.\n"
217 | + f"{emoji.bullet2} **Duration**: `{format_timedelta(dur, locale='en_IN')}`\n"
218 | + f"{emoji.bullet2} **Reason**: {reason}",
219 | color=config.color.error,
220 | )
221 | await ctx.respond(embed=timeout_em)
222 |
223 | # Untimeout user
224 | @slash_command(name="untimeout")
225 | @discord.default_permissions(moderate_members=True)
226 | @option("user", description="Mention the user whom you want to untimeout")
227 | @option("reason", description="Enter the reason for user timeout", required=False)
228 | async def untimeout_user(self, ctx: discord.ApplicationContext, user: discord.Member, reason: str = None):
229 | """Untimeouts the mentioned user."""
230 | if user == ctx.author:
231 | error_em = discord.Embed(
232 | description=f"{emoji.error} You cannot use it on yourself", color=config.color.error
233 | )
234 | await ctx.respond(embed=error_em, ephemeral=True)
235 | elif user.top_role.position >= ctx.author.top_role.position:
236 | error_em = discord.Embed(
237 | description=f"{emoji.error} Given user has same role or higher role than you",
238 | color=config.color.error,
239 | )
240 | await ctx.respond(embed=error_em, ephemeral=True)
241 | else:
242 | await user.timeout(None, reason=reason)
243 | untimeout_em = discord.Embed(
244 | title=f"{emoji.timer} Untimed out User",
245 | description=f"Successfully untimed out {user.mention}.\n" + f"{emoji.bullet} **Reason**: {reason}",
246 | color=config.color.theme,
247 | )
248 | await ctx.respond(embed=untimeout_em)
249 |
250 | # Lock
251 | @slash_command(name="lock")
252 | @discord.default_permissions(manage_channels=True)
253 | @option("reason", description="Enter the reason for locking the channel", required=False)
254 | async def lock(self, ctx: discord.ApplicationContext, reason: str = None):
255 | """Locks the current channel."""
256 | lock_em = discord.Embed(
257 | title=f"{emoji.lock} Channel Locked",
258 | description=f"Successfull locked {ctx.channel.mention}.\n" + f"{emoji.bullet} **Reason**: {reason}",
259 | color=config.color.theme,
260 | )
261 | await ctx.channel.set_permissions(ctx.author, send_messages=True)
262 | await ctx.channel.set_permissions(ctx.guild.default_role, send_messages=False)
263 | await ctx.respond(embed=lock_em)
264 |
265 | # Unlock
266 | @slash_command(name="unlock")
267 | @discord.default_permissions(manage_channels=True)
268 | async def unlock(self, ctx: discord.ApplicationContext):
269 | """Unlocks the current channel."""
270 | unlock_em = discord.Embed(
271 | title=f"{emoji.unlock} Channel Unlocked",
272 | description=f"Successfull unlocked {ctx.channel.mention}",
273 | color=config.color.theme,
274 | )
275 | await ctx.channel.set_permissions(ctx.author, send_messages=True)
276 | await ctx.channel.set_permissions(ctx.guild.default_role, send_messages=True)
277 | await ctx.respond(embed=unlock_em)
278 |
279 | # Role slash cmd group
280 | role = SlashCommandGroup(
281 | name="role",
282 | description="Role related commands.",
283 | default_member_permissions=discord.Permissions(manage_roles=True),
284 | )
285 |
286 | # Role add
287 | @role.command(name="add")
288 | @option("user", description="Mention the user whom you want to add the role")
289 | @option("role", description="Mention the role which you will add to the user")
290 | async def add_role(self, ctx: discord.ApplicationContext, user: discord.Member, role: discord.Role):
291 | """Adds the mentioned role to the mentioned user."""
292 | add_role_em = discord.Embed(
293 | title=f"{emoji.plus} Role Added",
294 | description=f"Successfully added {role.mention} to {user.mention}",
295 | color=config.color.theme,
296 | )
297 | await user.add_roles(role)
298 | await ctx.respond(embed=add_role_em)
299 |
300 | # Remove role
301 | @role.command(name="remove")
302 | @option("user", description="Mention the user whom you want to remove the role")
303 | @option("role", description="Mention the role which you will remove from the user")
304 | async def remove_role(self, ctx: discord.ApplicationContext, user: discord.Member, role: discord.Role):
305 | """Removes the mentioned role from the mentioned user."""
306 | remove_role_em = discord.Embed(
307 | title=f"{emoji.minus} Role Removed",
308 | description=f"Successfully removed {role.mention} from {user.mention}",
309 | color=config.color.theme,
310 | )
311 | await user.remove_roles(role)
312 | await ctx.respond(embed=remove_role_em)
313 |
314 |
315 | def setup(client: discord.Bot):
316 | client.add_cog(Moderation(client))
317 |
--------------------------------------------------------------------------------
/cogs/music.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import datetime
3 | import discord
4 | import discord.ui
5 | import lavalink
6 | import math
7 | import re
8 | from babel.dates import format_timedelta
9 | from discord.commands import option, slash_command
10 | from discord.ext import commands, tasks
11 | from music import equalizer_presets, store
12 | from music.client import LavalinkVoiceClient
13 | from music.sources import spotify
14 | from utils import config
15 | from utils.emoji import emoji
16 |
17 | # Regex
18 | url_rx = re.compile("https?:\\/\\/(?:www\\.)?.+")
19 |
20 |
21 | class MusicView(discord.ui.View):
22 | def __init__(self, client: discord.Bot, timeout: int):
23 | super().__init__(timeout=timeout, disable_on_timeout=True)
24 | self.client = client
25 |
26 | async def interaction_check(self, interaction: discord.Interaction):
27 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
28 | if not player.current:
29 | error_em = discord.Embed(
30 | description=f"{emoji.error} Nothing is being played at the current moment", color=config.color.error
31 | )
32 | await interaction.response.send_message(embed=error_em, ephemeral=True)
33 | return False
34 | elif not interaction.user.voice:
35 | error_em = discord.Embed(description=f"{emoji.error} Join a voice channel first", color=config.color.error)
36 | await interaction.response.send_message(embed=error_em, ephemeral=True)
37 | return False
38 | elif player.is_connected and interaction.user.voice.channel.id != int(player.channel_id):
39 | error_em = discord.Embed(
40 | description=f"{emoji.error} You are not in my voice channel", color=config.color.error
41 | )
42 | await interaction.response.send_message(embed=error_em, ephemeral=True)
43 | else:
44 | return True
45 |
46 | # Pause / Resume
47 | @discord.ui.button(emoji=f"{emoji.pause2}", custom_id="pause", style=discord.ButtonStyle.grey)
48 | async def pause_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
49 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
50 | if not player.paused:
51 | await player.set_pause(True)
52 | elif player.paused:
53 | await player.set_pause(False)
54 | button.emoji = emoji.play2 if player.paused else emoji.pause2
55 | pause_em = discord.Embed(
56 | title=f"{emoji.play if player.paused else emoji.pause} Player {'Paused' if player.paused else 'Resumed'}",
57 | description=f"{interaction.user.mention} {'Paused' if player.paused else 'Resumed'} the player",
58 | color=config.color.theme,
59 | )
60 | await interaction.response.edit_message(view=self)
61 | await interaction.followup.send(embed=pause_em, delete_after=5)
62 |
63 | # Stop
64 | @discord.ui.button(emoji=f"{emoji.stop2}", custom_id="stop", style=discord.ButtonStyle.grey)
65 | async def stop_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
66 | guild: discord.Guild = self.client.get_guild(int(interaction.guild_id))
67 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(int(interaction.guild_id))
68 | if player:
69 | player.channel_id = False
70 | await player.stop()
71 | player.queue.clear()
72 | stop_embed = discord.Embed(
73 | title=f"{emoji.stop} Player Destroyed",
74 | description=f"{interaction.user.mention} Destroyed the player",
75 | color=config.color.theme,
76 | )
77 | for child in self.children:
78 | child.disabled = True
79 | await interaction.response.edit_message(view=self)
80 | await guild.voice_client.disconnect(force=True)
81 | await interaction.followup.send(embed=stop_embed, delete_after=5)
82 | await Disable(self.client, guild.id).queue_msg()
83 |
84 | # Skip
85 | @discord.ui.button(emoji=f"{emoji.skip2}", custom_id="skip", style=discord.ButtonStyle.grey)
86 | async def skip_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
87 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
88 | await player.skip()
89 | skip_em = discord.Embed(
90 | title=f"{emoji.skip} Track Skipped",
91 | description=f"{interaction.user.mention} Skipped the track",
92 | color=config.color.theme,
93 | )
94 | for child in self.children:
95 | child.disabled = True
96 | await interaction.response.edit_message(view=self)
97 | await interaction.followup.send(embed=skip_em, delete_after=5)
98 |
99 | # Loop
100 | @discord.ui.button(emoji=f"{emoji.loop3}", custom_id="loop", style=discord.ButtonStyle.grey)
101 | async def loop_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
102 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
103 | if player.loop == player.LOOP_NONE:
104 | player.set_loop(1)
105 | button.emoji = emoji.loop2
106 | mode = "Track"
107 | elif player.loop == player.LOOP_SINGLE and player.queue:
108 | player.set_loop(2)
109 | button.emoji = emoji.loop
110 | mode = "Queue"
111 | else:
112 | player.set_loop(0)
113 | button.emoji = emoji.loop3
114 | mode = "OFF"
115 | loop_em = discord.Embed(
116 | title=f"{button.emoji} {mode if mode != 'OFF' else ''} Loop {'Enabled' if mode != 'OFF' else 'Disabled'}",
117 | description=f"Successfully {'enabled' if mode != 'OFF' else 'disabled'} {mode if mode != 'OFF' else ''} Loop",
118 | color=config.color.theme,
119 | )
120 | await interaction.response.edit_message(view=self)
121 | await interaction.followup.send(embed=loop_em, delete_after=5)
122 |
123 | # Shuffle
124 | @discord.ui.button(emoji=f"{emoji.shuffle2}", custom_id="shuffle", style=discord.ButtonStyle.grey)
125 | async def shuffle_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
126 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
127 | if not player.queue:
128 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
129 | await interaction.response.send_message(embed=error_em, ephemeral=True)
130 | else:
131 | player.shuffle = not player.shuffle
132 | button.emoji = f"{emoji.shuffle if player.shuffle else emoji.shuffle2}"
133 | shuffle_em = discord.Embed(
134 | title=f"{button.emoji} Shuffle {'Enabled' if player.shuffle else 'Disabled'}",
135 | description=f"{interaction.user.mention} {'Enabled' if player.shuffle else 'Disabled'} shuffle",
136 | color=config.color.theme,
137 | )
138 | await interaction.response.edit_message(view=self)
139 | await interaction.followup.send(embed=shuffle_em, delete_after=5)
140 |
141 |
142 | class QueueView(discord.ui.View):
143 | def __init__(self, client: discord.Bot, page: int, timeout: int):
144 | super().__init__(timeout=timeout, disable_on_timeout=True)
145 | self.client = client
146 | self.page = page
147 | self.items_per_page = 5
148 |
149 | async def interaction_check(self, interaction: discord.Interaction):
150 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
151 | if not player.queue:
152 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
153 | await interaction.response.send_message(embed=error_em, ephemeral=True)
154 | else:
155 | return True
156 |
157 | # Start
158 | @discord.ui.button(emoji=f"{emoji.start}", custom_id="start", style=discord.ButtonStyle.grey)
159 | async def start_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
160 | self.page = 1
161 | queue_embed = await QueueEmbed(self.client, interaction, self.page).get_embed()
162 | await interaction.response.edit_message(embed=queue_embed, view=self)
163 |
164 | # Previous
165 | @discord.ui.button(emoji=f"{emoji.previous}", custom_id="previous", style=discord.ButtonStyle.grey)
166 | async def previous_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
167 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
168 | pages = math.ceil(len(player.queue) / self.items_per_page)
169 | if self.page <= 1:
170 | self.page = pages
171 | else:
172 | self.page -= 1
173 | queue_embed = await QueueEmbed(self.client, interaction, self.page).get_embed()
174 | await interaction.response.edit_message(embed=queue_embed, view=self)
175 |
176 | # Next
177 | @discord.ui.button(emoji=f"{emoji.next}", custom_id="next", style=discord.ButtonStyle.grey)
178 | async def next_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
179 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
180 | pages = math.ceil(len(player.queue) / self.items_per_page)
181 | if self.page >= pages:
182 | self.page = 1
183 | else:
184 | self.page += 1
185 | queue_embed = await QueueEmbed(self.client, interaction, self.page).get_embed()
186 | await interaction.response.edit_message(embed=queue_embed, view=self)
187 |
188 | # End
189 | @discord.ui.button(emoji=f"{emoji.end}", custom_id="end", style=discord.ButtonStyle.grey)
190 | async def end_callback(self, button: discord.ui.Button, interaction: discord.Interaction):
191 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(interaction.guild_id)
192 | pages = math.ceil(len(player.queue) / self.items_per_page)
193 | self.page = pages
194 | queue_embed = await QueueEmbed(self.client, interaction, self.page).get_embed()
195 | await interaction.response.edit_message(embed=queue_embed, view=self)
196 |
197 |
198 | class QueueEmbed:
199 | def __init__(self, client: discord.Bot, ctx: discord.ApplicationContext, page: int):
200 | self.client = client
201 | self.ctx = ctx
202 | self.page = page
203 | self.items_per_page = 5
204 |
205 | async def get_embed(self) -> discord.Embed:
206 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(self.ctx.guild_id)
207 | pages = math.ceil(len(player.queue) / self.items_per_page)
208 | start = (self.page - 1) * self.items_per_page
209 | end = start + self.items_per_page
210 | current_requester = self.ctx.guild.get_member(player.current.requester)
211 | queue_list = ""
212 | for index, track in enumerate(player.queue[start:end], start=start):
213 | requester = self.ctx.guild.get_member(track.requester)
214 | queue_list += (
215 | f"`{index + 1}.` **[{track.title}]({track.uri})** [{requester.mention if requester else 'Unknown'}]\n"
216 | )
217 | queue_em = discord.Embed(title=f"{emoji.playlist} {self.ctx.guild.name}'s Queue", colour=config.color.theme)
218 | queue_em.add_field(
219 | name="Now Playing",
220 | value=f"`0.` **[{player.current.title}]({player.current.uri})** [{current_requester.mention if current_requester else 'Unknown'}]",
221 | inline=False,
222 | )
223 | queue_em.add_field(name=f"Queued {len(player.queue)} Track(s)", value=f"{queue_list}", inline=False)
224 | queue_em.set_footer(text=f"Viewing Page {self.page}/{pages}")
225 | return queue_em
226 |
227 |
228 | class Disable:
229 | def __init__(self, client: discord.Bot, guild_id: int):
230 | self.client = client
231 | self.guild_id = guild_id
232 |
233 | # Disable queue menu
234 | async def edit_messages_async(self, queue_view, messages) -> None:
235 | tasks = [msg.edit(view=queue_view) for msg in messages]
236 | await asyncio.gather(*tasks)
237 |
238 | async def queue_msg(self) -> None:
239 | if len(store.queue_msg(self.guild_id)) > 0:
240 | queue_view = QueueView(self.client, page=1, timeout=None)
241 | for child in queue_view.children:
242 | child.disabled = True
243 | await self.edit_messages_async(queue_view, store.queue_msg(self.guild_id))
244 | store.queue_msg(self.guild_id, "clear")
245 |
246 | # Disable play message
247 | async def play_msg(self) -> None:
248 | play_msg = store.play_msg(self.guild_id)
249 | music_view = MusicView(self.client, timeout=None)
250 | for child in music_view.children:
251 | child.disabled = True
252 | await play_msg.edit(view=music_view)
253 |
254 |
255 | class Music(commands.Cog):
256 | def __init__(self, client: discord.Bot):
257 | self.client = client
258 | self.music.start()
259 |
260 | # Looping music task
261 | @tasks.loop(seconds=0)
262 | async def music(self):
263 | await self.client.wait_until_ready()
264 | if not hasattr(self.client, "lavalink"):
265 | self.client.lavalink = lavalink.Client(self.client.user.id)
266 | self.client.lavalink.add_node(
267 | config.lavalink["host"], config.lavalink["port"], config.lavalink["password"], "us", "default-node"
268 | )
269 | self.client.lavalink.add_event_hook(self.track_hook)
270 |
271 | # Current voice
272 | def current_voice_channel(self, ctx: discord.ApplicationContext):
273 | if ctx.guild and ctx.guild.me.voice:
274 | return ctx.guild.me.voice.channel
275 | return None
276 |
277 | # Unloading cog
278 | def cog_unload(self):
279 | self.client.lavalink._event_hooks.clear()
280 |
281 | # Lavalink track hook event
282 | async def track_hook(self, event: lavalink.Event):
283 | if isinstance(event, lavalink.events.TrackStartEvent):
284 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(int(event.player.guild_id))
285 | channel = store.play_ch_id(event.player.guild_id)
286 | requester = f"<@{player.current.requester}>"
287 | if player.current.stream:
288 | duration = "🔴 LIVE"
289 | else:
290 | duration = datetime.timedelta(milliseconds=player.current.duration)
291 | duration = format_timedelta(duration, locale="en_IN")
292 | play_em = discord.Embed(
293 | title=f"{player.current.title}",
294 | url=f"{player.current.uri}",
295 | description=f"{emoji.bullet} **Requested By**: {requester if requester else 'Unknown'}\n"
296 | + f"{emoji.bullet} **Duration**: `{duration}`",
297 | color=config.color.theme,
298 | )
299 | if player.current.source_name == "spotify":
300 | play_em.set_thumbnail(url=player.current.extra["cover"])
301 | elif player.current.source_name == "youtube":
302 | play_em.set_image(url=f"https://i.ytimg.com/vi/{player.current.identifier}/maxresdefault.jpg")
303 | music_view = MusicView(self.client, timeout=None)
304 | # Loop emoji
305 | if player.loop == player.LOOP_NONE:
306 | music_view.children[3].emoji = emoji.loop3
307 | elif player.loop == player.LOOP_SINGLE:
308 | music_view.children[3].emoji = emoji.loop2
309 | else:
310 | music_view.children[3].emoji = emoji.loop
311 | # Shuffle emoji
312 | if player.shuffle:
313 | music_view.children[4].emoji = emoji.shuffle
314 | else:
315 | music_view.children[4].emoji = emoji.shuffle2
316 | # Player msg
317 | play_msg = await channel.send(embed=play_em, view=music_view)
318 | store.play_msg(event.player.guild_id, play_msg, "set")
319 | if isinstance(event, lavalink.events.TrackEndEvent):
320 | await Disable(self.client, event.player.guild_id).play_msg()
321 | if isinstance(event, lavalink.events.TrackStuckEvent):
322 | channel = store.play_ch_id(event.player.guild_id)
323 | error_em = discord.Embed(
324 | description=f"{emoji.error} Error while playing the track. Please try again later.",
325 | color=config.color.error,
326 | )
327 | await Disable(self.client, event.player.guild_id).play_msg()
328 | await channel.send(embed=error_em, delete_after=5)
329 | if isinstance(event, lavalink.events.TrackExceptionEvent):
330 | channel = store.play_ch_id(event.player.guild_id)
331 | error_em = discord.Embed(
332 | description=f"{emoji.error} Error while playing the track. Please try again later.",
333 | color=config.color.error,
334 | )
335 | await Disable(self.client, event.player.guild_id).play_msg()
336 | await channel.send(embed=error_em, delete_after=5)
337 | if isinstance(event, lavalink.events.QueueEndEvent):
338 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.get(int(event.player.guild_id))
339 | guild: discord.Guild = self.client.get_guild(int(event.player.guild_id))
340 | await player.clear_filters()
341 | await guild.voice_client.disconnect(force=True)
342 | await Disable(self.client, event.player.guild_id).queue_msg()
343 |
344 | # Ensures voice parameters
345 | async def ensure_voice(self, ctx: discord.ApplicationContext):
346 | """Checks all the voice parameters."""
347 | player: lavalink.DefaultPlayer = None
348 | if not ctx.author.voice or not ctx.author.voice.channel:
349 | error_em = discord.Embed(description=f"{emoji.error} Join a voice channel first", color=config.color.error)
350 | await ctx.respond(embed=error_em, ephemeral=True)
351 | else:
352 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.create(ctx.guild.id)
353 | permissions = ctx.author.voice.channel.permissions_for(ctx.me)
354 | if ctx.command.name in ("play") and self.current_voice_channel(ctx) is None:
355 | if self.client.lavalink.node_manager.available_nodes:
356 | await ctx.author.voice.channel.connect(cls=LavalinkVoiceClient)
357 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.create(ctx.guild.id)
358 | store.play_ch_id(ctx.guild.id, ctx.channel, "set")
359 | else:
360 | self.client.lavalink.add_node(
361 | config.lavalink["host"],
362 | config.lavalink["port"],
363 | config.lavalink["password"],
364 | "us",
365 | "default-node",
366 | )
367 | elif self.current_voice_channel(ctx) is not None and not self.client.lavalink.node_manager.available_nodes:
368 | self.client.lavalink.add_node(
369 | config.lavalink["host"], config.lavalink["port"], config.lavalink["password"], "us", "default-node"
370 | )
371 | elif not permissions.connect or not permissions.speak:
372 | player: lavalink.DefaultPlayer = None
373 | error_em = discord.Embed(
374 | description=f"{emoji.error} I need the `Connect` and `Speak` permissions",
375 | color=config.color.error,
376 | )
377 | await ctx.respond(embed=error_em, ephemeral=True)
378 | elif "play" not in ctx.command.name:
379 | if not player.current:
380 | player: lavalink.DefaultPlayer = None
381 | error_em = discord.Embed(
382 | description=f"{emoji.error} Nothing is being played at the current moment",
383 | color=config.color.error,
384 | )
385 | await ctx.respond(embed=error_em, ephemeral=True)
386 | elif ctx.author.voice.channel.id != int(player.channel_id):
387 | player: lavalink.DefaultPlayer = None
388 | error_em = discord.Embed(
389 | description=f"{emoji.error} You are not in my voice channel", color=config.color.error
390 | )
391 | await ctx.respond(embed=error_em, ephemeral=True)
392 | return player
393 |
394 | # Search autocomplete
395 | async def search(self, ctx: discord.AutocompleteContext):
396 | """Searches a track from a given query."""
397 | player: lavalink.DefaultPlayer = self.client.lavalink.player_manager.create(ctx.interaction.guild_id)
398 | if ctx.value != "":
399 | result = await player.node.get_tracks(f"ytsearch:{ctx.value}")
400 | tracks = []
401 | for track in result["tracks"]:
402 | track_name = ""
403 | if len(track["info"]["title"]) >= 97:
404 | track_name = f"{track['info']['title'][:97]}..."
405 | else:
406 | track_name = track["info"]["title"]
407 | tracks.append(track_name)
408 | return tracks
409 | else:
410 | return []
411 |
412 | # Voice state update event
413 | @commands.Cog.listener()
414 | async def on_voice_state_update(self, member: discord.Member, before, after):
415 | """Deafen yourself when joining a voice channel."""
416 | if member.id == member.guild.me.id and after.channel is None:
417 | if member.guild.voice_client:
418 | await member.guild.voice_client.disconnect(force=True)
419 | if member.id != member.guild.me.id or not after.channel:
420 | return
421 | my_perms = after.channel.permissions_for(member)
422 | if not after.deaf and my_perms.deafen_members:
423 | await member.edit(deafen=True)
424 |
425 | # Play
426 | @slash_command(name="play")
427 | @option("query", description="Enter your track name/link or playlist link", autocomplete=search)
428 | async def play(self, ctx: discord.ApplicationContext, query: str):
429 | """Searches and plays a track from a given query."""
430 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
431 | if player:
432 | await ctx.defer()
433 | query = query.strip("<>")
434 | embed = discord.Embed(color=config.color.theme)
435 | # Spotify
436 | if "open.spotify.com" in query:
437 | results = await spotify.SpotifySource(query, ctx.author.id).load_item(self.client.lavalink)
438 | if results["loadType"] == lavalink.LoadType.PLAYLIST:
439 | tracks = results["tracks"]
440 | for track in tracks:
441 | player.add(requester=ctx.author.id, track=track)
442 | embed.title = f"{emoji.playlist} Playlist Enqueued"
443 | embed.description = f"**{results['playlistInfo'].name}** with `{len(tracks)}` tracks"
444 | elif results["loadType"] == lavalink.LoadType.TRACK:
445 | track = results["tracks"][0]
446 | player.add(requester=ctx.author.id, track=track)
447 | embed.title = f"{emoji.music} Track Enqueued"
448 | embed.description = f"**[{track.title}]({track.uri})**"
449 | else:
450 | error_em = discord.Embed(
451 | description=f"{emoji.error} No track found from the given query", color=config.color.error
452 | )
453 | await ctx.respond(embed=error_em, ephemeral=True)
454 | await ctx.respond(embed=embed)
455 | # Others
456 | else:
457 | if not url_rx.match(query):
458 | if "soundcloud.com" in query:
459 | query = f"scsearch:{query}"
460 | elif "music.youtube.com" in query:
461 | query = f"ytmsearch:{query}"
462 | else:
463 | query = f"ytsearch:{query}"
464 | results = await player.node.get_tracks(query)
465 | if not results or not results["tracks"]:
466 | error_em = discord.Embed(
467 | description=f"{emoji.error} No track found from the given query", color=config.color.error
468 | )
469 | await ctx.respond(embed=error_em, ephemeral=True)
470 | if results["loadType"] == "PLAYLIST_LOADED":
471 | tracks = results["tracks"]
472 | for track in tracks:
473 | player.add(requester=ctx.author.id, track=track)
474 | embed.title = f"{emoji.playlist} Playlist Enqueued"
475 | embed.description = f"**{results['playlistInfo']['name']}** with `{len(tracks)}` tracks"
476 | await ctx.respond(embed=embed)
477 | elif results["tracks"]:
478 | track = results["tracks"][0]
479 | player.add(requester=ctx.author.id, track=track)
480 | embed.title = f"{emoji.music} Track Enqueued"
481 | embed.description = f"**[{track['info']['title']}]({track['info']['uri']})**"
482 | await ctx.respond(embed=embed)
483 | if not player.is_playing:
484 | await player.play()
485 |
486 | # Now playing
487 | @slash_command(name="now-playing")
488 | async def now_playing(self, ctx: discord.ApplicationContext):
489 | """Shows currently playing track."""
490 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
491 | if player:
492 | requester = ctx.guild.get_member(player.current.requester)
493 | if player.current.stream:
494 | duration = "🔴 LIVE"
495 | elif not player.current.stream:
496 | bar_length = 20
497 | filled_length = int(bar_length * player.position // float(player.current.duration))
498 | bar = emoji.filled_bar * filled_length + "" + emoji.empty_bar * (bar_length - filled_length)
499 | duration = lavalink.utils.format_time(player.current.duration)
500 | equalizer = store.equalizer(ctx.guild.id)
501 | loop = ""
502 | if player.loop == player.LOOP_NONE:
503 | loop = "Disabled"
504 | elif player.loop == player.LOOP_SINGLE:
505 | loop = "Track"
506 | elif player.loop == player.LOOP_QUEUE:
507 | loop = "Queue"
508 | play_em = discord.Embed(
509 | title=f"{player.current.title}",
510 | url=f"{player.current.uri}",
511 | description=f"{emoji.bullet} **Requested By**: {requester.mention if requester else 'Unknown'}\n"
512 | + f"{emoji.bullet} **Duration**: `{duration}`\n"
513 | + f"{emoji.bullet} **Volume**: `{player.volume}%`\n"
514 | + f"{emoji.bullet} **Loop**: {loop}\n"
515 | + f"{emoji.bullet} **Shuffle**: {'Enabled' if player.shuffle else 'Disabled'}\n"
516 | + f"{emoji.bullet} **Equalizer**: `{equalizer}`\n"
517 | + f"{bar}",
518 | color=config.color.theme,
519 | )
520 | await ctx.respond(embed=play_em)
521 |
522 | # Equalizer
523 | @slash_command(name="equalizer")
524 | @option(
525 | "equalizer", description="Choose your equalizer", choices=list(equalizer_presets.presets.keys()) + ["Reset"]
526 | )
527 | async def equalizer(self, ctx: discord.ApplicationContext, equalizer: str):
528 | """Equalizer to change track quality."""
529 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
530 | if player:
531 | if equalizer == "Reset":
532 | await player.clear_filters()
533 | eq_em = discord.Embed(
534 | title=f"{emoji.equalizer} Reset Equalizer",
535 | description="Reset the equalizer",
536 | color=config.color.theme,
537 | )
538 | store.equalizer(guild_id=ctx.guild.id, name="None", mode="set")
539 | else:
540 | for eq_name, eq_gains in equalizer_presets.presets.items():
541 | if eq_name == equalizer:
542 | eq = lavalink.Equalizer()
543 | eq.update(bands=eq_gains)
544 | await player.set_filter(eq)
545 | eq_em = discord.Embed(
546 | title=f"{emoji.equalizer} Equalizer Changed",
547 | description=f"Added `{equalizer}` equalizer",
548 | color=config.color.theme,
549 | )
550 | store.equalizer(guild_id=ctx.guild.id, name=equalizer, mode="set")
551 | await ctx.respond(embed=eq_em)
552 |
553 | # Stop
554 | @slash_command(name="stop")
555 | async def stop(self, ctx: discord.ApplicationContext):
556 | """Destroys the player."""
557 | await ctx.defer()
558 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
559 | if player:
560 | try:
561 | player.queue.clear()
562 | await player.stop()
563 | await ctx.guild.voice_client.disconnect(force=True)
564 | except Exception:
565 | pass
566 | stop_embed = discord.Embed(title=f"{emoji.stop} Player Destroyed", color=config.color.theme)
567 | disable = Disable(self.client, ctx.guild.id)
568 | await disable.play_msg()
569 | await ctx.respond(embed=stop_embed)
570 | await disable.queue_msg()
571 |
572 | # Seek
573 | @slash_command(name="seek")
574 | @option("seconds", description="Enter track position in seconds")
575 | async def seek(self, ctx: discord.ApplicationContext, seconds: int):
576 | """Seeks to a given position in a track."""
577 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
578 | if player:
579 | track_time = player.position + (seconds * 1000)
580 | if lavalink.utils.format_time(player.current.duration) > lavalink.utils.format_time(track_time):
581 | await player.seek(track_time)
582 | seek_em = discord.Embed(
583 | title=f"{emoji.seek} Track Seeked",
584 | description=f"Moved track to `{lavalink.utils.format_time(track_time)}`",
585 | color=config.color.theme,
586 | )
587 | await ctx.respond(embed=seek_em)
588 | elif lavalink.utils.format_time(player.current.duration) <= lavalink.utils.format_time(track_time):
589 | await self.skip()
590 |
591 | # Skip
592 | @slash_command(name="skip")
593 | async def skip(self, ctx: discord.ApplicationContext):
594 | """Skips the current playing track."""
595 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
596 | if player:
597 | skip_em = discord.Embed(
598 | title=f"{emoji.skip} Track Skipped", description="Skipped the track", color=config.color.theme
599 | )
600 | await Disable(self.client, ctx.guild.id).play_msg()
601 | await player.skip()
602 | await ctx.respond(embed=skip_em)
603 |
604 | # Skip to
605 | @slash_command(name="skip-to")
606 | @option("track", description="Enter your track index number to skip")
607 | async def skip_to(self, ctx: discord.ApplicationContext, track: int):
608 | """Skips to a given track in the queue."""
609 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
610 | if player:
611 | await ctx.defer()
612 | if track < 1 or track > len(player.queue):
613 | error_em = discord.Embed(
614 | description=f"{emoji.error} Track number must be between `1` and `{len(player.queue)}`",
615 | color=config.color.error,
616 | )
617 | await ctx.respond(embed=error_em, ephemeral=True)
618 | else:
619 | player.queue = player.queue[track - 1 :]
620 | await player.skip()
621 | skip_em = discord.Embed(
622 | title=f"{emoji.skip} Track Skipped",
623 | description=f"Skipped to track `{track}`",
624 | color=config.color.theme,
625 | )
626 | await ctx.respond(embed=skip_em)
627 |
628 | # Pause
629 | @slash_command(name="pause")
630 | async def pause(self, ctx: discord.ApplicationContext):
631 | """Pauses the player."""
632 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
633 | if player:
634 | if player.paused:
635 | error_em = discord.Embed(
636 | description=f"{emoji.error} Player is already paused", color=config.color.error
637 | )
638 | await ctx.respond(embed=error_em, ephemeral=True)
639 | else:
640 | await player.set_pause(True)
641 | pause_em = discord.Embed(
642 | title=f"{emoji.pause} Player Paused",
643 | description="Successfully paused the player",
644 | color=config.color.theme,
645 | )
646 | await ctx.respond(embed=pause_em)
647 |
648 | # Resume
649 | @slash_command(name="resume")
650 | async def resume(self, ctx: discord.ApplicationContext):
651 | """Resumes the player."""
652 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
653 | if player:
654 | if player.paused:
655 | await player.set_pause(False)
656 | resume_em = discord.Embed(
657 | title=f"{emoji.play} Player Resumed",
658 | description="Successfully resumed the player",
659 | color=config.color.theme,
660 | )
661 | await ctx.respond(embed=resume_em)
662 | else:
663 | error_em = discord.Embed(description=f"{emoji.error} Player is not paused", color=config.color.error)
664 | await ctx.respond(embed=error_em, ephemeral=True)
665 |
666 | # Volume
667 | @slash_command(name="volume")
668 | @option("volume", description="Enter your volume amount from 1 - 100")
669 | async def volume(self, ctx: discord.ApplicationContext, volume: int):
670 | """Changes the player's volume 1 - 100."""
671 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
672 | if player:
673 | volume_condition = [volume < 1, volume > 100]
674 | if any(volume_condition):
675 | error_em = discord.Embed(
676 | description=f"{emoji.error} Volume amount must be between `1` - `100`", color=config.color.error
677 | )
678 | await ctx.respond(embed=error_em, ephemeral=True)
679 | else:
680 | await player.set_volume(volume)
681 | vol_em = discord.Embed(
682 | title=f"{emoji.volume} Volume Changed",
683 | description=f"Successfully changed volume to `{player.volume}%`",
684 | color=config.color.theme,
685 | )
686 | await ctx.respond(embed=vol_em)
687 |
688 | # Queue
689 | @slash_command(name="queue")
690 | @option("page", description="Enter queue page number", default=1, required=False)
691 | async def queue(self, ctx: discord.ApplicationContext, page: int = 1):
692 | """Shows the player's queue."""
693 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
694 | if player:
695 | items_per_page = 5
696 | pages = math.ceil(len(player.queue) / items_per_page)
697 | if not player.queue:
698 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
699 | await ctx.respond(embed=error_em, ephemeral=True)
700 | elif page > pages or page < 1:
701 | error_em = discord.Embed(
702 | description=f"{emoji.error} Page has to be between `1` to `{pages}`", color=config.color.error
703 | )
704 | await ctx.respond(embed=error_em, ephemeral=True)
705 | else:
706 | queue_obj = QueueEmbed(self.client, ctx, page)
707 | queue_em = await queue_obj.get_embed()
708 | if pages > 1:
709 | queue_view = QueueView(client=self.client, page=page, timeout=60)
710 | queue_msg = await ctx.respond(embed=queue_em, view=queue_view)
711 | store.queue_msg(ctx.guild.id, queue_msg, "set")
712 | else:
713 | await ctx.respond(embed=queue_em)
714 |
715 | # Shuffle
716 | @slash_command(name="shuffle")
717 | async def shuffle(self, ctx: discord.ApplicationContext):
718 | """Shuffles the player's queue."""
719 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
720 | if player:
721 | if not player.queue:
722 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
723 | await ctx.respond(embed=error_em, ephemeral=True)
724 | else:
725 | player.shuffle = not player.shuffle
726 | shuffle_em = discord.Embed(
727 | title=f"{emoji.shuffle if player.shuffle else emoji.shuffle2} Shuffle {'Enabled' if player.shuffle else 'Disabled'}",
728 | description=f"Successfully {'enabled' if player.shuffle else 'disabled'} shuffle",
729 | color=config.color.theme,
730 | )
731 | await ctx.respond(embed=shuffle_em)
732 |
733 | # Loop
734 | @slash_command(name="loop")
735 | @option("mode", description="Enter loop mode", choices=["OFF", "Queue", "Track"])
736 | async def loop(self, ctx: discord.ApplicationContext, mode: str):
737 | """Loops the current queue until the command is invoked again or until a new track is enqueued."""
738 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
739 | if player:
740 | _emoji = ""
741 | if mode == "OFF":
742 | player.set_loop(0)
743 | _emoji = emoji.loop3
744 | elif mode == "Track":
745 | player.set_loop(1)
746 | _emoji = emoji.loop2
747 | elif mode == "Queue":
748 | if not player.queue:
749 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
750 | await ctx.respond(embed=error_em, ephemeral=True)
751 | return
752 | else:
753 | player.set_loop(2)
754 | _emoji = emoji.loop
755 | loop_em = discord.Embed(
756 | title=f"{_emoji} {mode if mode != 'OFF' else ''} Loop {'Enabled' if mode != 'OFF' else 'Disabled'}",
757 | description=f"Successfully {'enabled' if mode != 'OFF' else 'disabled'} {mode if mode != 'OFF' else ''} Loop",
758 | color=config.color.theme,
759 | )
760 | await ctx.respond(embed=loop_em)
761 |
762 | # Remove
763 | @slash_command(name="remove")
764 | @option("index", description="Enter your track index number")
765 | async def remove(self, ctx: discord.ApplicationContext, index: int):
766 | """Removes a track from the player's queue with the given index."""
767 | player: lavalink.DefaultPlayer = await self.ensure_voice(ctx)
768 | if player:
769 | if ctx.author.id == player.queue[index - 1].requester:
770 | if not player.queue:
771 | error_em = discord.Embed(description=f"{emoji.error} Queue is empty", color=config.color.error)
772 | await ctx.respond(embed=error_em, ephemeral=True)
773 | elif index > len(player.queue) or index < 1:
774 | error_em = discord.Embed(
775 | description=f"{emoji.error} Index has to be between `1` to `{len(player.queue)}`",
776 | color=config.color.error,
777 | )
778 | await ctx.respond(embed=error_em, ephemeral=True)
779 | else:
780 | removed = player.queue.pop(index - 1)
781 | remove_em = discord.Embed(
782 | title=f"{emoji.playlist} Track Removed",
783 | description=f"**{removed.title}**",
784 | color=config.color.theme,
785 | )
786 | await ctx.respond(embed=remove_em)
787 | else:
788 | error_em = discord.Embed(
789 | description=f"{emoji.error} Only requester can remove from the list", color=config.color.error
790 | )
791 | await ctx.respond(embed=error_em, ephemeral=True)
792 |
793 |
794 | def setup(client: discord.Bot):
795 | client.add_cog(Music(client))
796 |
--------------------------------------------------------------------------------
/cogs/settings.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from db.funcs.guild import (
3 | fetch_guild_settings,
4 | remove_guild,
5 | set_autorole,
6 | set_mod_cmd_log_channel,
7 | set_mod_log_channel,
8 | set_msg_log_channel,
9 | set_ticket_cmds,
10 | set_ticket_log_channel,
11 | )
12 | from discord.commands import SlashCommandGroup, option, slash_command
13 | from discord.ext import commands
14 | from utils import config
15 | from utils.emoji import emoji
16 |
17 |
18 | class Settings(commands.Cog):
19 | def __init__(self, client: discord.Bot):
20 | self.client = client
21 |
22 | # Settings
23 | @slash_command(name="settings")
24 | @discord.default_permissions(manage_channels=True)
25 | async def settings(self, ctx: discord.ApplicationContext):
26 | """Shows server settings."""
27 |
28 | # Fetch channel mention util func
29 | async def mention_ch(channel_id):
30 | return f"<#{channel_id}>" if channel_id else emoji.off
31 |
32 | guild_settings = await fetch_guild_settings(ctx.guild.id)
33 |
34 | mod_log_channel = await mention_ch(guild_settings.mod_log_channel_id)
35 | mod_log_cmd_channel = await mention_ch(guild_settings.mod_cmd_log_channel_id)
36 | msg_log_channel = await mention_ch(guild_settings.msg_log_channel_id)
37 | ticket = emoji.on if guild_settings.ticket_cmds else emoji.off
38 | ticket_log_channel = await mention_ch(guild_settings.ticket_log_channel_id)
39 |
40 | role_id = guild_settings.autorole
41 | autorole = ctx.guild.get_role(role_id).mention if (role_id and ctx.guild.get_role(role_id)) else emoji.off
42 | if role_id and not ctx.guild.get_role(role_id):
43 | await set_autorole(ctx.guild.id, None) # Reset autorole if role doesn't exist
44 |
45 | set_em = discord.Embed(
46 | title=f"{emoji.settings} {ctx.guild.name}'s Settings",
47 | description=f"{emoji.bullet} **Mod Log Channel**: {mod_log_channel}\n"
48 | + f"{emoji.bullet} **Mod Command Log Channel**: {mod_log_cmd_channel}\n"
49 | + f"{emoji.bullet} **Message Log Channel**: {msg_log_channel}\n"
50 | + f"{emoji.bullet} **Ticket Commands**: {ticket}\n"
51 | + f"{emoji.bullet} **Ticket Log Channel**: {ticket_log_channel}\n"
52 | + f"{emoji.bullet} **Autorole**: {autorole}",
53 | color=config.color.theme,
54 | )
55 | await ctx.respond(embed=set_em)
56 |
57 | # Settings slash cmd group
58 | setting = SlashCommandGroup(
59 | name="setting",
60 | description="Server settings commands.",
61 | default_member_permissions=discord.Permissions(manage_channels=True, moderate_members=True),
62 | )
63 |
64 | # Reset
65 | @setting.command(name="reset")
66 | @option(
67 | "setting",
68 | description="Setting to reset",
69 | choices=["All", "Mod Log", "Mod Command Log", "Message Log", "Ticket Commands", "Ticket Log", "Auto Role"],
70 | )
71 | async def reset_settings(self, ctx: discord.ApplicationContext, setting: str):
72 | """Resets server settings."""
73 | if setting.lower() == "all":
74 | await remove_guild(ctx.guild.id)
75 | else:
76 | match setting.lower():
77 | case "mod log":
78 | await set_mod_cmd_log_channel(ctx.guild.id, None)
79 | case "mod command log":
80 | await set_mod_cmd_log_channel(ctx.guild.id, None)
81 | case "message log":
82 | await set_msg_log_channel(ctx.guild.id, None)
83 | case "ticket commands":
84 | await set_ticket_cmds(ctx.guild.id, False)
85 | case "ticket log":
86 | await set_ticket_log_channel(ctx.guild.id, None)
87 | case "auto role":
88 | await set_autorole(ctx.guild.id, None)
89 | reset_em = discord.Embed(
90 | title=f"{emoji.settings} Reset Settings",
91 | description=f"Successfully reset the {setting.lower()} settings.",
92 | color=config.color.theme,
93 | )
94 | await ctx.respond(embed=reset_em)
95 |
96 | # Set mod log
97 | @setting.command(name="mod-log")
98 | @option("channel", description="Mention the mod log channel")
99 | async def set_mod_log(self, ctx: discord.ApplicationContext, channel: discord.TextChannel):
100 | """Sets mod log channel."""
101 | await set_mod_log_channel(ctx.guild.id, channel.id)
102 | logging_em = discord.Embed(
103 | title=f"{emoji.settings} Mod Log Settings",
104 | description=f"Successfully set mod log channel to {channel.mention}.",
105 | color=config.color.theme,
106 | )
107 | await ctx.respond(embed=logging_em)
108 |
109 | # Set mod cmd log
110 | @setting.command(name="mod-command-log")
111 | @option("channel", description="Mention the mod command log channel")
112 | async def set_mod_cmd_log(self, ctx: discord.ApplicationContext, channel: discord.TextChannel):
113 | """Sets mod command log channel."""
114 | await set_mod_cmd_log_channel(ctx.guild.id, channel.id)
115 | logging_em = discord.Embed(
116 | title=f"{emoji.settings} Mod Command Log Settings",
117 | description=f"Successfully set mod command log channel to {channel.mention}.",
118 | color=config.color.theme,
119 | )
120 | await ctx.respond(embed=logging_em)
121 |
122 | # Set message log
123 | @setting.command(name="message-log")
124 | @option("channel", description="Mention the message log channel")
125 | async def set_msg_log(self, ctx: discord.ApplicationContext, channel: discord.TextChannel):
126 | """Sets message log channel."""
127 | await set_msg_log_channel(ctx.guild.id, channel.id)
128 | logging_em = discord.Embed(
129 | title=f"{emoji.settings} Message Log Settings",
130 | description=f"Successfully set message log channel to {channel.mention}.",
131 | color=config.color.theme,
132 | )
133 | await ctx.respond(embed=logging_em)
134 |
135 | # Set ticket cmds
136 | @setting.command(name="ticket-commands")
137 | @option("status", description="Enable or disable ticket commands", choices=["Enable", "Disable"])
138 | async def set_ticket_cmds(self, ctx: discord.ApplicationContext, status: str):
139 | """Enables or disables ticket commands."""
140 | match status.lower():
141 | case "enable":
142 | await set_ticket_cmds(ctx.guild.id, True)
143 | case "disable":
144 | await set_ticket_cmds(ctx.guild.id, False)
145 | ticket_cmds_em = discord.Embed(
146 | title=f"{emoji.settings} Ticket Commands Settings",
147 | description=f"Successfully {status.lower()}d ticket commands.",
148 | color=config.color.theme,
149 | )
150 | await ctx.respond(embed=ticket_cmds_em)
151 |
152 | # Set ticket log
153 | @setting.command(name="ticket-log")
154 | @option("channel", description="Mention the ticket log channel")
155 | async def set_ticket_log(self, ctx: discord.ApplicationContext, channel: discord.TextChannel):
156 | """Sets ticket log channel."""
157 | await set_ticket_log_channel(ctx.guild.id, channel.id)
158 | logging_em = discord.Embed(
159 | title=f"{emoji.settings} Ticket Log Settings",
160 | description=f"Successfully set ticket log channel to {channel.mention}.",
161 | color=config.color.theme,
162 | )
163 | await ctx.respond(embed=logging_em)
164 |
165 | # Set autorole
166 | @setting.command(name="auto-role")
167 | @option("role", description="Mention the autorole")
168 | async def set_auto_role(self, ctx: discord.ApplicationContext, role: discord.Role):
169 | """Sets autorole. The bot will assign this role to new members."""
170 | if role >= ctx.guild.me.top_role:
171 | error_em = discord.Embed(
172 | description=f"{emoji.error} I can't assign roles higher than my top role.", color=config.color.error
173 | )
174 | await ctx.respond(embed=error_em, ephemeral=True)
175 | elif role.name == "@everyone":
176 | error_em = discord.Embed(
177 | description=f"{emoji.error} I can't assign the @everyone role.", color=config.color.error
178 | )
179 | await ctx.respond(embed=error_em, ephemeral=True)
180 | else:
181 | await set_autorole(ctx.guild.id, role.id)
182 | autorole_em = discord.Embed(
183 | title=f"{emoji.settings} Auto Role Settings",
184 | description=f"Successfully set autorole to {role.mention}.",
185 | color=config.color.theme,
186 | )
187 | await ctx.respond(embed=autorole_em)
188 |
189 |
190 | def setup(client: discord.Bot):
191 | client.add_cog(Settings(client))
192 |
--------------------------------------------------------------------------------
/cogs/tickets.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import discord
3 | import io
4 | from db.funcs.guild import fetch_guild_settings
5 | from discord.commands import SlashCommandGroup, option
6 | from discord.ext import commands
7 | from utils import config
8 | from utils.emoji import emoji
9 |
10 |
11 | class TicketTranscript:
12 | def __init__(self, channel: discord.TextChannel):
13 | self.channel = channel
14 |
15 | async def create(self):
16 | messages = await self.channel.history(limit=500).flatten()
17 | with io.StringIO() as file:
18 | for message in reversed(messages):
19 | if message.content:
20 | file.write(f"{message.author}: {message.content}\n")
21 | else:
22 | file.write(f"{message.author}: Embed/Attachment\n")
23 | file.seek(0)
24 | return discord.File(file, filename=f"ticket_{self.channel.id}.txt")
25 |
26 |
27 | class TicketView(discord.ui.View):
28 | def __init__(self):
29 | super().__init__(timeout=None)
30 |
31 | # Interaction check
32 | async def interaction_check(self, interaction: discord.Interaction):
33 | if interaction.user.guild_permissions.manage_channels:
34 | return True
35 | else:
36 | ticket_check_em = discord.Embed(
37 | description=f"{emoji.error} You don't have `Manage Channels` permission to use this command.",
38 | color=config.color.error,
39 | )
40 | await interaction.response.send_message(embed=ticket_check_em, ephemeral=True)
41 | return False
42 |
43 | # Ticket close button
44 | @discord.ui.button(label="Close", emoji=emoji.lock, style=discord.ButtonStyle.grey, custom_id="close_ticket")
45 | async def close_ticket(self, button: discord.ui.Button, interaction: discord.Interaction):
46 | self.disable_all_items()
47 | await interaction.response.edit_message(view=self)
48 | close_em = discord.Embed(
49 | title=f"{emoji.ticket2} Closing Ticket",
50 | description="Closing ticket in 5 seconds.\n"
51 | + f"{emoji.bullet} **Author**: <@{interaction.channel.name.split('-')[1]}>\n"
52 | + f"{emoji.bullet} **Closed By**: {interaction.user.mention}",
53 | color=config.color.theme,
54 | )
55 | await interaction.followup.send(embed=close_em)
56 | await asyncio.sleep(5)
57 | await interaction.channel.delete()
58 | channel_id = (await fetch_guild_settings(interaction.guild.id)).ticket_log_channel_id
59 | if channel_id is not None:
60 | logging_ch = await interaction.channel.guild.fetch_channel(channel_id)
61 | close_log_em = discord.Embed(
62 | title=f"{emoji.ticket2} Ticket Closed",
63 | description=f"{emoji.bullet} **Author**: <@{interaction.channel.name.split('-')[1]}>\n"
64 | + f"{emoji.bullet} **Closed By**: {interaction.user.mention}",
65 | color=config.color.theme,
66 | )
67 | await logging_ch.send(embed=close_log_em)
68 |
69 | # Ticket summary
70 | @discord.ui.button(
71 | label="Transcript", emoji=emoji.embed, style=discord.ButtonStyle.grey, custom_id="ticket_summary"
72 | )
73 | async def ticket_summary(self, button: discord.ui.Button, interaction: discord.Interaction):
74 | button.disabled = True
75 | await interaction.response.edit_message(view=self)
76 | await interaction.channel.trigger_typing()
77 | file = await TicketTranscript(interaction.channel).create()
78 | await interaction.followup.send(
79 | embed=discord.Embed(description=f"**Requested by**: {interaction.user.mention}", color=config.color.theme),
80 | file=file,
81 | )
82 | await asyncio.sleep(2)
83 | button.disabled = False
84 | await interaction.message.edit(view=self)
85 |
86 |
87 | class Tickets(commands.Cog):
88 | def __init__(self, client):
89 | self.client = client
90 |
91 | # Ticket slash cmd group
92 | ticket = SlashCommandGroup(name="ticket", description="Ticket related commands.")
93 |
94 | # Ticket create
95 | @ticket.command(name="create")
96 | @option("reason", description="Enter your reason for creating the ticket", required=False)
97 | async def create_ticket(self, ctx: discord.ApplicationContext, reason: str = "No reason provided"):
98 | """Creates a ticket."""
99 | ticket_status = (await fetch_guild_settings(ctx.guild.id)).ticket_cmds
100 | if not ticket_status:
101 | error_em = discord.Embed(
102 | description=f"{emoji.error} Ticket commands are disabled", color=config.color.error
103 | )
104 | await ctx.respond(embed=error_em, ephemeral=True)
105 | else:
106 | await ctx.defer()
107 | category = discord.utils.get(ctx.guild.categories, name="Tickets")
108 | if category is None:
109 | category = await ctx.guild.create_category("Tickets")
110 | await category.set_permissions(ctx.guild.default_role, view_channel=False)
111 | await category.set_permissions(ctx.guild.me, view_channel=True)
112 | create_ch = await category.create_text_channel(f"ticket-{ctx.author.id}")
113 | await create_ch.set_permissions(
114 | ctx.author,
115 | view_channel=True,
116 | send_messages=True,
117 | read_messages=True,
118 | add_reactions=True,
119 | embed_links=True,
120 | attach_files=True,
121 | read_message_history=True,
122 | external_emojis=True,
123 | )
124 | await create_ch.set_permissions(
125 | ctx.guild.me,
126 | view_channel=True,
127 | send_messages=True,
128 | read_messages=True,
129 | add_reactions=True,
130 | embed_links=True,
131 | attach_files=True,
132 | read_message_history=True,
133 | external_emojis=True,
134 | )
135 | await create_ch.set_permissions(
136 | ctx.guild.default_role, view_channel=False, send_messages=False, read_messages=False
137 | )
138 |
139 | create_em = discord.Embed(
140 | title=f"{emoji.ticket} Ticket Created",
141 | description="Thank you for creating the ticket. Your problem will be solved soon! Stay tuned!\n"
142 | + f"{emoji.bullet} **Author**: {ctx.author.mention}\n"
143 | + f"{emoji.bullet} **Reason**: {reason}",
144 | color=config.color.theme,
145 | )
146 | await create_ch.send(ctx.author.mention, embed=create_em, view=TicketView())
147 | create_done_em = discord.Embed(
148 | title=f"{emoji.ticket} Ticket Created",
149 | description=f"Successfully created {create_ch.mention}.",
150 | color=config.color.theme,
151 | )
152 | await ctx.respond(embed=create_done_em)
153 |
154 | log_ch_id = (await fetch_guild_settings(ctx.guild.id)).ticket_log_channel_id
155 | if log_ch_id is not None:
156 | logging_ch = await self.client.fetch_channel(log_ch_id)
157 | create_log_em = discord.Embed(
158 | title=f"{emoji.ticket} Ticket Created",
159 | description=f"{emoji.bullet} **Author**: {ctx.author.mention}\n"
160 | + f"{emoji.bullet} **Reason**: {reason}",
161 | color=config.color.theme,
162 | )
163 | await logging_ch.send(embed=create_log_em)
164 |
165 | # Ticket close
166 | @ticket.command(name="close")
167 | async def close_ticket(self, ctx: discord.ApplicationContext):
168 | """Closes a created ticket."""
169 | ticket_status = (await fetch_guild_settings(ctx.guild.id)).ticket_cmds
170 | if not ticket_status:
171 | error_em = discord.Embed(
172 | description=f"{emoji.error} Ticket commands are disabled", color=config.color.error
173 | )
174 | await ctx.respond(embed=error_em, ephemeral=True)
175 | else:
176 | if (ctx.channel.name == f"ticket-{ctx.author.id}") or (
177 | ctx.channel.name.startswith("ticket-") and ctx.author.guild_permissions.manage_channels
178 | ):
179 | close_em = discord.Embed(
180 | title=f"{emoji.ticket2} Closing Ticket",
181 | description="Closing ticket in 5 seconds.\n" + f"{emoji.bullet} **Author**: {ctx.author.mention}",
182 | color=config.color.theme,
183 | )
184 | await ctx.respond(embed=close_em)
185 | await asyncio.sleep(5)
186 | await ctx.channel.delete()
187 | log_ch_id = (await fetch_guild_settings(ctx.guild.id)).ticket_log_channel_id
188 | if log_ch_id is not None:
189 | logging_ch = await self.client.fetch_channel(log_ch_id)
190 | close_log_em = discord.Embed(
191 | title=f"{emoji.ticket2} Ticket Closed",
192 | description=f"{emoji.bullet} **Author**: <@{ctx.channel.name.split('-')[1]}>\n"
193 | + f"{emoji.bullet} **Closed By**: {ctx.author.mention}",
194 | color=config.color.theme,
195 | )
196 | await logging_ch.send(embed=close_log_em)
197 | else:
198 | error_em = discord.Embed(
199 | description=f"{emoji.error} This is not a ticket channel", color=config.color.error
200 | )
201 | await ctx.respond(embed=error_em, ephemeral=True)
202 |
203 | # Ticket transcript
204 | @ticket.command(name="transcript")
205 | @commands.cooldown(1, 10, commands.BucketType.user)
206 | async def transcript_ticket(self, ctx: discord.ApplicationContext):
207 | """Transcript an opened ticket."""
208 | if (ctx.channel.name == f"ticket-{ctx.author.id}") or (
209 | ctx.channel.name.startswith("ticket-") and ctx.author.guild_permissions.manage_channels
210 | ):
211 | await ctx.defer()
212 | file = await TicketTranscript(ctx.channel).create()
213 | await ctx.respond(file=file)
214 | else:
215 | error_em = discord.Embed(
216 | description=f"{emoji.error} This is not a ticket channel", color=config.color.error
217 | )
218 | await ctx.respond(embed=error_em, ephemeral=True)
219 |
220 | # Add ticket view on restart
221 | @commands.Cog.listener()
222 | async def on_ready(self):
223 | self.client.add_view(TicketView())
224 |
225 |
226 | def setup(client: discord.Bot):
227 | client.add_cog(Tickets(client))
228 |
--------------------------------------------------------------------------------
/db/__init__.py:
--------------------------------------------------------------------------------
1 | from rich.progress import Progress, SpinnerColumn
2 | from tortoise import Tortoise
3 | from utils.config import db_url
4 |
5 | TORTOISE_ORM = {
6 | "connections": {
7 | "default": db_url,
8 | },
9 | "apps": {"models": {"models": ["db.schema", "aerich.models"], "default_connection": "default"}},
10 | }
11 |
12 |
13 | class DB:
14 | """Database class to handle Tortoise ORM initialization and connection management."""
15 |
16 | async def init(self):
17 | """Initialize the database connection and generate schemas."""
18 | db_prog = Progress(
19 | SpinnerColumn(style="yellow", finished_text="[green bold]✓[/]"),
20 | "[progress.description]{task.description}",
21 | )
22 |
23 | with db_prog as prog:
24 | db_task = prog.add_task("Initializing Database", total=1)
25 | await Tortoise.init(TORTOISE_ORM)
26 | await Tortoise.generate_schemas()
27 | prog.update(db_task, description="[green]Initialized Database[/]", completed=1)
28 |
29 | async def close(self):
30 | """Close the database connection."""
31 | await Tortoise.close_connections()
32 |
--------------------------------------------------------------------------------
/db/funcs/dev.py:
--------------------------------------------------------------------------------
1 | from ..schema import DevTable
2 |
3 |
4 | async def fetch_dev_ids() -> list[int]:
5 | """Fetches all developer user IDs from the database."""
6 | devs = await DevTable.all().values_list("user_id", flat=True)
7 | return list(devs)
8 |
9 |
10 | async def add_dev(user_id: int) -> None:
11 | """
12 | Adds a developer user ID to the database.
13 |
14 | Parameters:
15 | user_id (int): The user ID to perform action on.
16 | """
17 | await DevTable.get_or_create(user_id=user_id)
18 |
19 |
20 | async def remove_dev(user_id: int) -> None:
21 | """
22 | Removes a developer user ID from the database.
23 |
24 | Parameters:
25 | user_id (int): The user ID to perform action on.
26 | """
27 | dev = await DevTable.filter(user_id=user_id).first()
28 | if dev:
29 | await dev.delete()
30 | else:
31 | raise ValueError(f"User ID {user_id} is not a developer.")
32 |
--------------------------------------------------------------------------------
/db/funcs/guild.py:
--------------------------------------------------------------------------------
1 | from ..schema import GuildTable
2 |
3 |
4 | async def fetch_guild_ids() -> list[int]:
5 | """Fetches all guild IDs from the database."""
6 | guilds = await GuildTable.all().values_list("guild_id", flat=True)
7 | return list(guilds)
8 |
9 |
10 | async def add_guild(guild_id: int) -> GuildTable:
11 | """
12 | Adds a guild to the database.
13 |
14 | Parameters:
15 | guild_id (int): The guild ID to perform action on.
16 | """
17 | return (await GuildTable.get_or_create(guild_id=guild_id))[0]
18 |
19 |
20 | async def remove_guild(guild_id: int) -> None:
21 | """
22 | Removes a guild from the database.
23 |
24 | Parameters:
25 | guild_id (int): The guild ID to perform action on.
26 | """
27 | guild = await GuildTable.filter(guild_id=guild_id).first()
28 | if guild:
29 | await guild.delete()
30 | else:
31 | pass
32 |
33 |
34 | async def fetch_guild_settings(guild_id: int) -> GuildTable:
35 | """
36 | Fetches settings for a specific guild.
37 |
38 | Parameters:
39 | guild_id (int): The guild ID to fetch settings for.
40 | """
41 | guild = await GuildTable.filter(guild_id=guild_id).first()
42 | if not guild:
43 | guild = guild = await add_guild(guild_id)
44 | return guild
45 |
46 |
47 | async def set_mod_log_channel(guild_id: int, channel_id: int) -> None:
48 | """
49 | Sets the mod log channel for a guild.
50 |
51 | Parameters:
52 | guild_id (int): The guild ID to perform action on.
53 | channel_id (int): The channel ID to set as the mod log channel.
54 | """
55 | guild = await GuildTable.filter(guild_id=guild_id).first()
56 | if not guild:
57 | guild = await add_guild(guild_id)
58 | guild.mod_log_channel_id = channel_id
59 | await guild.save()
60 |
61 |
62 | async def set_mod_cmd_log_channel(guild_id: int, channel_id: int) -> None:
63 | """
64 | Sets the mod command log channel for a guild.
65 |
66 | Parameters:
67 | guild_id (int): The guild ID to perform action on.
68 | channel_id (int): The channel ID to set as the mod command log channel.
69 | """
70 | guild = await GuildTable.filter(guild_id=guild_id).first()
71 | if not guild:
72 | guild = await add_guild(guild_id)
73 | guild.mod_cmd_log_channel_id = channel_id
74 | await guild.save()
75 |
76 |
77 | async def set_msg_log_channel(guild_id: int, channel_id: int) -> None:
78 | """
79 | Sets the message log channel for a guild.
80 |
81 | Parameters:
82 | guild_id (int): The guild ID to perform action on.
83 | channel_id (int): The channel ID to set as the message log channel.
84 | """
85 | guild = await GuildTable.filter(guild_id=guild_id).first()
86 | if not guild:
87 | guild = await add_guild(guild_id)
88 | guild.msg_log_channel_id = channel_id
89 | await guild.save()
90 |
91 |
92 | async def set_ticket_cmds(guild_id: int, enabled: bool) -> None:
93 | """
94 | Enables or disables ticket commands for a guild.
95 |
96 | Parameters:
97 | guild_id (int): The guild ID to perform action on.
98 | enabled (bool): Whether to enable or disable ticket commands.
99 | """
100 | guild = await GuildTable.filter(guild_id=guild_id).first()
101 | if not guild:
102 | guild = await add_guild(guild_id)
103 | guild.ticket = enabled
104 | await guild.save()
105 |
106 |
107 | async def set_ticket_log_channel(guild_id: int, channel_id: int) -> None:
108 | """
109 | Sets the ticket log channel for a guild.
110 |
111 | Parameters:
112 | guild_id (int): The guild ID to perform action on.
113 | channel_id (int): The channel ID to set as the ticket log channel.
114 | """
115 | guild = await GuildTable.filter(guild_id=guild_id).first()
116 | if not guild:
117 | guild = await add_guild(guild_id)
118 | guild.ticket_log_channel_id = channel_id
119 | await guild.save()
120 |
121 |
122 | async def set_autorole(guild_id: int, role_id: int) -> None:
123 | """
124 | Sets the autorole for a guild.
125 |
126 | Parameters:
127 | guild_id (int): The guild ID to perform action on.
128 | role_id (int): The role ID to set as the autorole.
129 | """
130 | guild = await GuildTable.filter(guild_id=guild_id).first()
131 | if not guild:
132 | guild = await add_guild(guild_id)
133 | guild.autorole = role_id
134 | await guild.save()
135 |
--------------------------------------------------------------------------------
/db/schema.py:
--------------------------------------------------------------------------------
1 | from tortoise import fields
2 | from tortoise.models import Model
3 |
4 |
5 | class DevTable(Model):
6 | id = fields.IntField(primary_key=True)
7 | user_id = fields.BigIntField(unique=True)
8 | joined_at = fields.DatetimeField(auto_now_add=True)
9 |
10 | class Meta:
11 | table = "dev"
12 |
13 |
14 | class GuildTable(Model):
15 | id = fields.IntField(primary_key=True)
16 | guild_id = fields.BigIntField(unique=True)
17 | mod_log_channel_id = fields.BigIntField(null=True)
18 | mod_cmd_log_channel_id = fields.BigIntField(null=True)
19 | msg_log_channel_id = fields.BigIntField(null=True)
20 | ticket_cmds = fields.BooleanField(default=True)
21 | ticket_log_channel_id = fields.BigIntField(null=True)
22 | autorole = fields.BigIntField(null=True)
23 |
24 | class Meta:
25 | table = "guild"
26 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | name: square
2 |
3 | services:
4 | bot:
5 | build: .
6 | container_name: "square"
7 | restart: unless-stopped
8 |
--------------------------------------------------------------------------------
/example.config.toml:
--------------------------------------------------------------------------------
1 | owner-id = 0
2 | owner-guild-ids = []
3 | system-channel-id = 0
4 | support-server-url = "https://discord.gg" # Replace with your support server URL.
5 | emoji = "default"
6 |
7 | bot-token = ""
8 | database-url = "asyncpg://..." # If it starts with "postgresql://", replace it with "asyncpg://".
9 |
10 | [colors]
11 | theme = "#1FCEEC"
12 | error = "#E74C3C"
13 |
14 | [lavalink]
15 | host = ""
16 | port = 0
17 | password = ""
18 | secure = false
19 |
20 | [spotify]
21 | client_id = ""
22 | client_secret = ""
23 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import discord
2 | import os
3 | from db import DB
4 | from pyfiglet import Figlet
5 | from rich import print
6 | from rich.progress import Progress, SpinnerColumn
7 | from utils import config
8 |
9 | # Discord vars
10 | status = discord.Status.online
11 | activity = discord.Activity(type=discord.ActivityType.listening, name="Discord")
12 | intents = discord.Intents.all()
13 | client = discord.Bot(status=status, activity=activity, intents=intents, help_command=None)
14 |
15 | # Startup printing
16 | figlted_txt = Figlet(font="standard", justify="center").renderText("Discord Bot")
17 | print(f"[cyan]{figlted_txt}[/]")
18 |
19 |
20 | # On ready event
21 | @client.event
22 | async def on_ready():
23 | print(f"[green][bold]✓[/] Logged in as [cyan]{client.user}[/] [ID: {client.user.id}][/]")
24 | print(f"[green][bold]✓[/] Connected to {len(client.guilds)} guild{'' if len(client.guilds) <= 1 else 's'}[/]")
25 |
26 |
27 | # Loading all files
28 | def load_cogs():
29 | cogs_prog = Progress(
30 | SpinnerColumn(style="yellow", finished_text="[green bold]✓[/]"),
31 | "[progress.description]{task.description} [progress.percentage]{task.percentage:>3.1f}%",
32 | )
33 | with cogs_prog as prog:
34 | file_count = len([file for file in os.listdir("./cogs") if file.endswith(".py")])
35 | task = cogs_prog.add_task("Loading Cogs", total=file_count)
36 | for filename in os.listdir("./cogs"):
37 | if filename.endswith(".py"):
38 | prog.update(task, advance=1)
39 | client.load_extension(f"cogs.{filename[:-3]}")
40 | prog.update(task, description="[green]Loaded Cogs[/]", completed=file_count)
41 |
42 |
43 | # Shutdown
44 | async def shutdown():
45 | """Shutdown the bot gracefully."""
46 | shutdown_prog = Progress(
47 | SpinnerColumn(style="yellow", finished_text="[yellow bold]✓[/]"),
48 | "[progress.description]{task.description}",
49 | )
50 |
51 | with shutdown_prog as prog:
52 | task = prog.add_task("Shutting down", total=2)
53 | await DB().close()
54 | prog.advance(task, advance=1)
55 | if not client.is_closed():
56 | await client.close()
57 | prog.advance(task, advance=1)
58 | prog.update(task, description="[yellow]Bot has been shut down[/]", completed=2)
59 |
60 |
61 | # Main func to run the bot
62 | async def main():
63 | try:
64 | await DB().init()
65 | load_cogs()
66 | await client.start(config.bot_token)
67 | finally:
68 | await shutdown()
69 |
70 |
71 | # Execute the main func
72 | try:
73 | client.loop.run_until_complete(main())
74 | except Exception as e:
75 | print(f"[red][bold]✗[/] Unable to login due to {e}[/]")
76 |
--------------------------------------------------------------------------------
/music/client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import discord
3 | import lavalink
4 | from utils import config
5 |
6 |
7 | class LavalinkVoiceClient(discord.VoiceProtocol):
8 | def __init__(self, client: discord.Bot, channel: discord.abc.Connectable):
9 | self.client = client
10 | self.channel = channel
11 | self.connect_event = asyncio.Event()
12 |
13 | async def on_voice_server_update(self, data):
14 | lavalink_data = {"t": "VOICE_SERVER_UPDATE", "d": data}
15 | await self.lavalink.voice_update_handler(lavalink_data)
16 |
17 | async def on_voice_state_update(self, data):
18 | lavalink_data = {"t": "VOICE_STATE_UPDATE", "d": data}
19 | await self.lavalink.voice_update_handler(lavalink_data)
20 |
21 | # Connect
22 | async def connect(self, *, timeout: float, reconnect: bool) -> None:
23 | await self.channel.guild.change_voice_state(channel=self.channel)
24 | try:
25 | self.lavalink: lavalink.Client = self.client.lavalink
26 | except AttributeError:
27 | self.client.lavalink = self.lavalink = lavalink.Client(self.client.user.id)
28 | self.client.lavalink.add_node(
29 | host=config.lavalink["host"],
30 | port=config.lavalink["port"],
31 | password=config.lavalink["password"],
32 | name="default-node",
33 | ssl=config.lavalink["secure"],
34 | )
35 |
36 | # Disconnect
37 | async def disconnect(self, *, force: bool) -> None:
38 | await self.channel.guild.change_voice_state(channel=None)
39 | player: lavalink.DefaultPlayer = self.lavalink.player_manager.get(self.channel.guild.id)
40 | if player:
41 | player.channel_id = False
42 | await player.stop()
43 | self.cleanup()
44 |
--------------------------------------------------------------------------------
/music/equalizer_presets.py:
--------------------------------------------------------------------------------
1 | presets = {
2 | "Bass Boost": [(0, 0.10), (1, 0.15), (2, 0.20), (3, 0.25), (4, 0.35), (5, 0.45), (6, 0.50), (7, 0.55)],
3 | "Jazz": [(0, 0.18), (1, 0.16), (2, 0.12), (3, 0.09), (4, 0.08), (5, 0.08), (6, 0.12), (7, 0.18)],
4 | "Pop": [(0, 0.18), (1, 0.14), (2, 0.10), (3, 0.08), (4, 0.06), (5, 0.06), (6, 0.10), (7, 0.18)],
5 | "Treble": [(0, 0.55), (1, 0.50), (2, 0.40), (3, 0.30), (4, 0.20), (5, 0.10), (6, 0.05), (7, 0.02)],
6 | "Nightcore": [(0, 0.30), (1, 0.30), (2, 0.30), (3, 0.30), (4, 0.30), (5, 0.30), (6, 0.30), (7, 0.30)],
7 | "Super Bass": [(0, 0.20), (1, 0.30), (2, 0.40), (3, 0.50), (4, 0.60), (5, 0.70), (6, 0.80), (7, 0.90)],
8 | }
9 |
--------------------------------------------------------------------------------
/music/sources/spotify.py:
--------------------------------------------------------------------------------
1 | import discord
2 | import lavalink
3 | import spotipy
4 | from spotipy.oauth2 import SpotifyClientCredentials
5 | from utils import config
6 |
7 | sp = spotipy.Spotify(
8 | auth_manager=SpotifyClientCredentials(
9 | client_id=config.spotify["client_id"],
10 | client_secret=config.spotify["client_secret"],
11 | cache_handler=spotipy.cache_handler.MemoryCacheHandler(),
12 | )
13 | )
14 |
15 |
16 | class SpotifyAudioTrack(lavalink.DeferredAudioTrack):
17 | async def load(self, client: discord.Bot):
18 | result: lavalink.LoadResult = await client.get_tracks(f"ytsearch:{self.title} {self.author}")
19 | if result.load_type != lavalink.LoadType.SEARCH or not result.tracks:
20 | raise lavalink.LoadError
21 | first_track = result.tracks[0]
22 | base64 = first_track.track
23 | self.track = base64
24 | return base64
25 |
26 |
27 | class SpotifySource(lavalink.Source):
28 | def __init__(self, url: str, requester: int):
29 | self.url = url
30 | self.requester = requester
31 | super().__init__(name="spotify")
32 |
33 | # Spotify source
34 | async def get(self):
35 | return {"playlist": sp.playlist, "album": sp.album, "track": sp.track}.get(
36 | self.url.split("/")[-2], lambda _: None
37 | )(self.url)
38 |
39 | # Load playlist
40 | async def _load_pl(self) -> tuple[list[SpotifyAudioTrack], lavalink.PlaylistInfo]:
41 | pl = await self.get()
42 | tracks = []
43 | for track in pl["tracks"]["items"]:
44 | tracks.append(
45 | SpotifyAudioTrack(
46 | {
47 | "identifier": track["track"]["id"],
48 | "isSeekable": True,
49 | "author": ", ".join([artist["name"] for artist in track["track"]["artists"]]),
50 | "length": track["track"]["duration_ms"],
51 | "isStream": False,
52 | "title": track["track"]["name"],
53 | "uri": track["track"]["external_urls"]["spotify"],
54 | "sourceName": "spotify",
55 | },
56 | requester=self.requester,
57 | cover=track["track"]["album"]["images"][0]["url"],
58 | )
59 | )
60 | pl_info = lavalink.PlaylistInfo(name=pl["name"])
61 | return tracks, pl_info
62 |
63 | # Load album
64 | async def _load_al(self) -> tuple[list[SpotifyAudioTrack], lavalink.PlaylistInfo]:
65 | al = await self.get()
66 | tracks = []
67 | for track in al["tracks"]["items"]:
68 | tracks.append(
69 | SpotifyAudioTrack(
70 | {
71 | "identifier": track["id"],
72 | "isSeekable": True,
73 | "author": ", ".join([artist["name"] for artist in track["artists"]]),
74 | "length": track["duration_ms"],
75 | "isStream": False,
76 | "title": track["name"],
77 | "uri": track["external_urls"]["spotify"],
78 | "sourceName": "spotify",
79 | },
80 | requester=self.requester,
81 | cover=track["album"]["images"][0]["url"],
82 | )
83 | )
84 | pl_info = lavalink.PlaylistInfo(name=al["name"])
85 | return tracks, pl_info
86 |
87 | # Load track
88 | async def _load_track(self) -> SpotifyAudioTrack:
89 | track = await self.get()
90 | return SpotifyAudioTrack(
91 | {
92 | "identifier": track["id"],
93 | "isSeekable": True,
94 | "author": ", ".join([artist["name"] for artist in track["artists"]]),
95 | "length": track["duration_ms"],
96 | "isStream": False,
97 | "title": track["name"],
98 | "uri": track["external_urls"]["spotify"],
99 | "sourceName": "spotify",
100 | },
101 | requester=self.requester,
102 | cover=track["album"]["images"][0]["url"],
103 | )
104 |
105 | # Load items
106 | async def load_item(self, client: discord.Bot):
107 | if "playlist" in self.url:
108 | pl, pl_info = await self._load_pl()
109 | return lavalink.LoadResult(lavalink.LoadType.PLAYLIST, pl, pl_info)
110 | if "album" in self.url:
111 | al, al_info = await self._load_al()
112 | return lavalink.LoadResult(lavalink.LoadType.PLAYLIST, al, al_info)
113 | if "track" in self.url:
114 | track = await self._load_track()
115 | return lavalink.LoadResult(lavalink.LoadType.TRACK, [track], lavalink.PlaylistInfo.none())
116 |
--------------------------------------------------------------------------------
/music/store.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Literal
2 |
3 | obj = {}
4 |
5 |
6 | # Play channel ID
7 | def play_ch_id(guild_id: int, channel_id: Any | None = None, mode: Literal["get", "set"] = "get"):
8 | """
9 | Gets or sets the play channel ID for a guild.
10 |
11 | Parameters:
12 | guild_id (int): The ID of the guild.
13 | channel_id (Any | None): The channel ID to set, or None to get the current value.
14 | mode (str): The operation mode, either "get" or "set".
15 | """
16 | match mode:
17 | case "get":
18 | return obj.get(f"{str(guild_id)}-play_ch_id", None)
19 | case "set":
20 | obj.update({f"{str(guild_id)}-play_ch_id": channel_id})
21 |
22 |
23 | # Play msg ID
24 | def play_msg(guild_id: int, msg: Any | None = None, mode: Literal["get", "set"] = "get"):
25 | """
26 | Gets or sets the play message ID for a guild.
27 |
28 | Parameters:
29 | guild_id (int): The ID of the guild.
30 | msg (Any | None): The message ID to set, or None to get the current value.
31 | mode (str): The operation mode, either "get" or "set".
32 | """
33 | match mode:
34 | case "get":
35 | return obj.get(f"{str(guild_id)}-play_msg", None)
36 | case "set":
37 | obj.update({f"{str(guild_id)}-play_msg": msg})
38 |
39 |
40 | # Queue msg object
41 | def queue_msg(guild_id: int, msg: Any | None = None, mode: Literal["get", "set", "clear"] = "get"):
42 | """
43 | Gets or sets the queue message for a guild.
44 |
45 | Parameters:
46 | guild_id (int): The ID of the guild.
47 | msg (Any | None): The message object to set, or None to get the current value.
48 | mode (str): The operation mode, either "get", "set", or "clear".
49 | """
50 | match mode:
51 | case "get":
52 | if obj.__contains__(f"{str(guild_id)}-queue_msgs"):
53 | return obj.get(f"{str(guild_id)}-queue_msgs", None)
54 | else:
55 | return []
56 | case "set":
57 | if obj.__contains__(f"{str(guild_id)}-queue_msgs"):
58 | obj[f"{str(guild_id)}-queue_msgs"].append(msg)
59 | else:
60 | obj.update({f"{str(guild_id)}-queue_msgs": [msg]})
61 | case "clear":
62 | obj.update({f"{str(guild_id)}-queue_msgs": []})
63 |
64 |
65 | # Equalizer
66 | def equalizer(guild_id: int, name: str = None, mode: Literal["get", "set"] = "get"):
67 | """
68 | Gets or sets the equalizer settings for a guild.
69 |
70 | Parameters:
71 | guild_id (int): The ID of the guild.
72 | name (str | None): The name of the equalizer to set, or None to get the current value.
73 | mode (str): The operation mode, either "get" or "set".
74 | """
75 | match mode:
76 | case "get":
77 | return obj.get(f"{str(guild_id)}-equalizer", None)
78 | case "set":
79 | obj.update({f"{str(guild_id)}-equalizer": name})
80 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "square"
3 | version = "1.1.0"
4 | description = "Advanced multipurpose discord bot for all your needs."
5 | license = "GPL-3.0"
6 | readme = "README.md"
7 | authors = [{ name = "Swayam" }]
8 | requires-python = ">=3.13"
9 | dependencies = [
10 | "aerich[toml]>=0.9.0",
11 | "aiohttp>=3.12.4",
12 | "babel>=2.17.0",
13 | "lavalink>=5.9.0",
14 | "py-cord",
15 | "pyfiglet>=1.0.2",
16 | "requests>=2.32.3",
17 | "rich>=14.0.0",
18 | "spotipy>=2.25.1",
19 | "toml>=0.10.2",
20 | "tortoise-orm[asyncpg]>=0.25.0",
21 | ]
22 |
23 | [tool.uv.sources.py-cord]
24 | git = "https://github.com/Pycord-Development/pycord"
25 |
26 | [tool.ruff]
27 | line-length = 120
28 |
29 | [tool.ruff.lint]
30 | select = ["E", "W", "F", "I", "B", "C4", "UP"]
31 | ignore = ["E501"]
32 |
33 | [tool.ruff.lint.isort]
34 | no-sections = true
35 |
36 | [tool.ruff.format]
37 | quote-style = "double"
38 | indent-style = "space"
39 | docstring-code-format = true
40 |
41 | [tool.aerich]
42 | tortoise_orm = "db.TORTOISE_ORM"
43 | location = "./migrations"
44 | src_folder = "./."
45 |
46 | [dependency-groups]
47 | dev = ["ruff>=0.11.12"]
48 |
--------------------------------------------------------------------------------
/utils/check.py:
--------------------------------------------------------------------------------
1 | import discord
2 | from db.funcs.dev import fetch_dev_ids
3 | from discord.ext import commands
4 | from utils import config
5 |
6 |
7 | def is_owner():
8 | """Check if the command invoker is the bot owner."""
9 |
10 | async def predicate(ctx: discord.ApplicationContext):
11 | owner_id = config.owner_id
12 | if ctx.author.id == owner_id:
13 | return True
14 | else:
15 | raise commands.CommandError("You are not authorized to use this command.")
16 |
17 | return commands.check(predicate)
18 |
19 |
20 | def is_dev():
21 | """Check if the command invoker is a developer or the bot owner."""
22 |
23 | async def predicate(ctx: discord.ApplicationContext):
24 | owner_id = config.owner_id
25 | dev_ids = await fetch_dev_ids()
26 | if ctx.author.id == owner_id or ctx.author.id in dev_ids:
27 | return True
28 | else:
29 | raise commands.MissingPermissions("You are not authorized to use this command.")
30 |
31 | return commands.check(predicate)
32 |
--------------------------------------------------------------------------------
/utils/config.py:
--------------------------------------------------------------------------------
1 | import toml
2 | from attr import dataclass
3 | from typing import TypedDict
4 |
5 | config_file_path = "./config.toml"
6 |
7 | # Load the configuration file
8 | with open(config_file_path) as f:
9 | data = toml.load(f)
10 |
11 |
12 | # Bot configuration
13 | owner_id: int = data["owner-id"]
14 | owner_guild_ids: list[int] = data["owner-guild-ids"]
15 | system_channel_id: int = data["system-channel-id"]
16 | support_server_url: str = data["support-server-url"]
17 | emoji_type: str = data["emoji"]
18 | bot_token: str = data["bot-token"]
19 | db_url: str = data["database-url"]
20 |
21 |
22 | # Colors
23 | @dataclass
24 | class ColorConfig:
25 | theme: int
26 | error: int
27 |
28 |
29 | def colors() -> ColorConfig:
30 | """Returns the color configuration."""
31 | color = data["colors"]
32 | for key, value in color.items():
33 | color[key] = int(value.replace("#", ""), 16)
34 | return ColorConfig(**color)
35 |
36 |
37 | color = colors()
38 |
39 |
40 | # Lavalink configuration
41 | class LavalinkConfig(TypedDict):
42 | host: str
43 | port: int
44 | password: str
45 | secure: bool
46 |
47 |
48 | lavalink: LavalinkConfig = data["lavalink"]
49 |
50 |
51 | # Spotify configuration
52 | class SpotifyConfig(TypedDict):
53 | client_id: str
54 | client_secret: str
55 |
56 |
57 | spotify: SpotifyConfig = data["spotify"]
58 |
--------------------------------------------------------------------------------
/utils/emoji.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from attr import dataclass
4 | from rich import print
5 | from utils import config
6 |
7 | custom_emoji_file_path = "./.cache/emoji.json"
8 | if not any([config.emoji_type == "custom", config.emoji_type == "default"]):
9 | print(f"[red][bold]✗[/] Invalid emoji type in [cyan]config.toml[/]: {config.emoji_type}[/]")
10 | print("[yellow][bold]![/] Please choose either [green]custom[/] or [green]default[/].[/]")
11 | exit(1)
12 |
13 |
14 | # Dataclass
15 | @dataclass
16 | class Emoji:
17 | bullet: str = "▸"
18 | bullet2: str = "▹"
19 | success: str = "✅"
20 | error: str = "❌"
21 |
22 | on: str = "🟢"
23 | off: str = "🔴"
24 |
25 | embed: str = "📜"
26 | edit: str = "✏️"
27 | bin: str = "🗑️"
28 |
29 | plus: str = "➕"
30 | minus: str = "➖"
31 | next: str = "➡️"
32 | previous: str = "⬅️"
33 | start: str = "⏮️"
34 | end: str = "⏭️"
35 |
36 | kick: str = "🦵🏻"
37 | info: str = "📑"
38 | mod: str = "🔨"
39 | mod2: str = "🔨"
40 | mass_mod: str = "💪🏻"
41 | timer: str = "🕛"
42 | timer2: str = "🕛"
43 | lock: str = "🔒"
44 | unlock: str = "🔓"
45 | settings: str = "⚙️"
46 |
47 | ticket: str = "🎟️"
48 | ticket2: str = "🎟️"
49 |
50 | music: str = "🎵"
51 | play: str = "▶️"
52 | play2: str = "▶️"
53 | pause: str = "⏸️"
54 | pause2: str = "⏸️"
55 | stop: str = "⏹️"
56 | stop2: str = "⏹️"
57 | skip: str = "⏭️"
58 | skip2: str = "⏭️"
59 | shuffle: str = "🔀"
60 | shuffle2: str = "🔀"
61 | seek: str = "⏩"
62 | loop: str = "🔁"
63 | loop2: str = "🔂"
64 | loop3: str = "🔁"
65 | playlist: str = "📃"
66 | volume: str = "🔊"
67 | equalizer: str = "🎶"
68 | filled_bar: str = "⬜"
69 | empty_bar: str = "🟥"
70 |
71 | upload: str = "📤"
72 | console: str = "⌨️"
73 | restart: str = "🔄️"
74 | shutdown: str = "🛑"
75 |
76 | @staticmethod
77 | def from_json(file_path: str) -> "Emoji":
78 | try:
79 | with open(file_path, encoding="utf8") as emoji_file:
80 | emoji_data = json.load(emoji_file)
81 |
82 | # Validate keys
83 | missing_keys = [key for key in Emoji.__annotations__.keys() if key not in emoji_data]
84 | extra_keys = [key for key in emoji_data if key not in Emoji.__annotations__.keys()]
85 |
86 | if missing_keys:
87 | print(f"[red][bold]✗[/] Missing keys in emoji JSON: [cyan]{missing_keys}[/][/]")
88 | exit(1)
89 | if extra_keys:
90 | print(f"[yellow][bold]![/] Extra keys in emoji JSON: [cyan]{extra_keys}[/][/]")
91 |
92 | # Create Emoji instance
93 | return Emoji(**{key: emoji_data.get(key, "") for key in Emoji.__annotations__.keys()})
94 |
95 | except FileNotFoundError:
96 | if config.emoji_type == "custom":
97 | print(f"[red][bold]✗[/] Custom emoji file not found: {file_path}[/]")
98 | print(
99 | "[yellow][bold]![/] Make sure to run [cyan]/emoji upload[/] command and upload emojis to the discord bot and run [cyan]/emoji sync[/] to create required config files.[/]"
100 | )
101 | print(
102 | "[yellow][bold]![/] If already uploaded, run [cyan]/emoji sync[/] to create required config files.[/]"
103 | )
104 | print(
105 | "[yellow][bold]![/] If you want to use default emojis, change the emoji type in [cyan]config.toml[/] to [green]default[/].[/]"
106 | )
107 | else:
108 | print(f"[red][bold]✗[/] Emoji file not found: {file_path}[/]")
109 | print(
110 | "[yellow][bold]![/] Seems like default emoji file is missing. Please download the default emoji file from the repository and place it in the [green]configs[/] folder.[/]"
111 | )
112 | exit(1)
113 | except json.JSONDecodeError:
114 | print(f"[red][bold]✗[/] Invalid JSON format in file: {file_path}[/]")
115 |
116 | @staticmethod
117 | def create_custom_emoji_config(emojis: dict) -> dict:
118 | os.makedirs(os.path.dirname(custom_emoji_file_path), exist_ok=True)
119 | with open(custom_emoji_file_path, "w", encoding="utf8") as emoji_file:
120 | missing_keys = [key for key in Emoji.__annotations__.keys() if key not in emojis]
121 | extra_keys = [emojis[key] for key in emojis if key not in Emoji.__annotations__.keys()]
122 | if missing_keys:
123 | return {"status": "error", "missing_keys": missing_keys}
124 | else:
125 | emojis = {key: emojis[key] for key in Emoji.__annotations__.keys()}
126 | json.dump(emojis, emoji_file, ensure_ascii=False, indent=4)
127 | msg = {"status": "success"}
128 | if extra_keys:
129 | msg["extra_keys"] = extra_keys
130 | return msg
131 |
132 |
133 | emoji = Emoji.from_json(custom_emoji_file_path) if config.emoji_type == "custom" else Emoji()
134 |
--------------------------------------------------------------------------------
/utils/helpers.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import re
3 |
4 |
5 | def parse_duration(duration: str) -> datetime.timedelta:
6 | """
7 | Parse a duration string into a timedelta object.
8 |
9 | Parameters:
10 | duration (str): A string representing the duration, e.g., "2w3d4h5m6s".
11 | """
12 | pattern = re.compile(r"(?P\d+)(?P[wdhms])")
13 | matches = pattern.findall(duration)
14 |
15 | if not matches:
16 | raise ValueError(
17 | "Invalid duration format.\n-# Use `w` for weeks, `d` for days, `h` for hours, `m` for minutes, and `s` for seconds."
18 | )
19 |
20 | total_duration = datetime.timedelta()
21 | funcs = {
22 | "w": lambda x: datetime.timedelta(weeks=x),
23 | "d": lambda x: datetime.timedelta(days=x),
24 | "h": lambda x: datetime.timedelta(hours=x),
25 | "m": lambda x: datetime.timedelta(minutes=x),
26 | "s": lambda x: datetime.timedelta(seconds=x),
27 | }
28 |
29 | for value, unit in matches:
30 | value = int(value)
31 | if value <= 0:
32 | raise ValueError("Duration values must be positive.")
33 | total_duration += funcs[unit](value)
34 |
35 | if total_duration.total_seconds() <= 0:
36 | raise ValueError("Total duration must be positive.")
37 | elif total_duration.days > 28:
38 | raise ValueError("Total duration must be less than `28 days`.")
39 |
40 | return total_duration
41 |
42 |
43 | def fmt_perms(perms: list[str]) -> str:
44 | """
45 | Format a list of permissions into a human-readable string.
46 |
47 | Parameters:
48 | perms (list[str]): A list of permission names.
49 |
50 | Returns:
51 | str: A formatted string of permissions.
52 | """
53 | perms = [perm.replace("_", " ").replace("guild", "server").title() for perm in perms]
54 | if len(perms) > 2:
55 | return "{}, and {}".format("**, **".join(perms[:-1]), perms[-1])
56 | elif len(perms) == 2:
57 | return "{} and {}".format("**".join(perms[:-1]), perms[-1])
58 | elif perms:
59 | return perms[0]
60 | else:
61 | return "No permissions"
62 |
--------------------------------------------------------------------------------