Skip to content

Commit 1ada010

Browse files
authored
Implement custom prefixes (#61)
1 parent 6035020 commit 1ada010

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed

bot/cogs/config.py

+113-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Optional
3+
from typing import TYPE_CHECKING, Annotated, Optional, Union
44

55
import asyncpg
66
import discord
77
import msgspec
88
from async_lru import alru_cache
9+
from discord import app_commands
910
from discord.ext import commands
1011
from libs.utils import GuildContext
1112
from libs.utils.checks import bot_check_permissions, check_permissions
13+
from libs.utils.embeds import Embed
14+
from libs.utils.prefix import get_prefix
1215

1316
if TYPE_CHECKING:
1417
from rodhaj import Rodhaj
@@ -96,6 +99,16 @@ class SetupFlags(commands.FlagConverter):
9699
)
97100

98101

102+
class PrefixConverter(commands.Converter):
103+
async def convert(self, ctx: commands.Context, argument: str):
104+
user_id = ctx.bot.user.id
105+
if argument.startswith((f"<@{user_id}>", f"<@!{user_id}>")):
106+
raise commands.BadArgument("That is a reserved prefix already in use.")
107+
if len(argument) > 100:
108+
raise commands.BadArgument("That prefix is too long.")
109+
return argument
110+
111+
99112
class Config(commands.Cog):
100113
"""Config and setup commands for Rodhaj"""
101114

@@ -120,6 +133,12 @@ async def get_guild_config(self, guild_id: int) -> Optional[GuildConfig]:
120133
config = GuildConfig(bot=self.bot, **dict(rows))
121134
return config
122135

136+
def clean_prefixes(self, prefixes: Union[str, list[str]]) -> str:
137+
if isinstance(prefixes, str):
138+
return f"`{prefixes}`"
139+
140+
return ", ".join(f"`{prefix}`" for prefix in prefixes[2:])
141+
123142
@check_permissions(manage_guild=True)
124143
@bot_check_permissions(manage_channels=True, manage_webhooks=True)
125144
@commands.guild_only()
@@ -334,6 +353,99 @@ async def delete(self, ctx: GuildContext) -> None:
334353
else:
335354
await ctx.send("Cancelling.")
336355

356+
@check_permissions(manage_guild=True)
357+
@commands.guild_only()
358+
@config.group(name="prefix", fallback="info")
359+
async def prefix(self, ctx: GuildContext) -> None:
360+
"""Shows and manages custom prefixes for the guild
361+
362+
Passing in no subcommands will effectively show the currently set prefixes.
363+
"""
364+
prefixes = await get_prefix(self.bot, ctx.message)
365+
embed = Embed()
366+
embed.add_field(
367+
name="Prefixes", value=self.clean_prefixes(prefixes), inline=False
368+
)
369+
embed.add_field(name="Total", value=len(prefixes) - 2, inline=False)
370+
embed.set_author(name=ctx.guild.name, icon_url=ctx.guild.icon.url) # type: ignore
371+
await ctx.send(embed=embed)
372+
373+
@prefix.command(name="add")
374+
@app_commands.describe(prefix="The new prefix to add")
375+
async def prefix_add(
376+
self, ctx: GuildContext, prefix: Annotated[str, PrefixConverter]
377+
) -> None:
378+
"""Adds an custom prefix"""
379+
prefixes = await get_prefix(self.bot, ctx.message)
380+
381+
# 2 are the mention prefixes, which are always prepended on the list of prefixes
382+
if isinstance(prefixes, list) and len(prefixes) > 12:
383+
await ctx.send(
384+
"You can not have more than 10 custom prefixes for your server"
385+
)
386+
return
387+
elif prefix in prefixes:
388+
await ctx.send("The prefix you want to set already exists")
389+
return
390+
391+
query = """
392+
UPDATE guild_config
393+
SET prefix = ARRAY_APPEND(prefix, $1)
394+
WHERE id = $2;
395+
"""
396+
await self.pool.execute(query, prefix, ctx.guild.id)
397+
get_prefix.cache_invalidate(self.bot, ctx.message)
398+
await ctx.send(f"Added prefix: `{prefix}`")
399+
400+
@prefix.command(name="edit")
401+
@app_commands.describe(
402+
old="The prefix to edit", new="A new prefix to replace the old"
403+
)
404+
@app_commands.rename(old="old_prefix", new="new_prefix")
405+
async def prefix_edit(
406+
self,
407+
ctx: GuildContext,
408+
old: Annotated[str, PrefixConverter],
409+
new: Annotated[str, PrefixConverter],
410+
) -> None:
411+
"""Edits and replaces a prefix"""
412+
query = """
413+
UPDATE guild_config
414+
SET prefix = ARRAY_REPLACE(prefix, $1, $2)
415+
WHERE id = $3;
416+
"""
417+
prefixes = await get_prefix(self.bot, ctx.message)
418+
419+
guild_id = ctx.guild.id
420+
if old in prefixes:
421+
await self.pool.execute(query, old, new, guild_id)
422+
get_prefix.cache_invalidate(self.bot, ctx.message)
423+
await ctx.send(f"Prefix updated to from `{old}` to `{new}`")
424+
else:
425+
await ctx.send("The prefix is not in the list of prefixes for your server")
426+
427+
@prefix.command(name="delete")
428+
@app_commands.describe(prefix="The prefix to delete")
429+
async def prefix_delete(
430+
self, ctx: GuildContext, prefix: Annotated[str, PrefixConverter]
431+
) -> None:
432+
"""Deletes a set prefix"""
433+
query = """
434+
UPDATE guild_config
435+
SET prefix = ARRAY_REMOVE(prefix, $1)
436+
WHERE id=$2;
437+
"""
438+
msg = f"Do you want to delete the following prefix: {prefix}"
439+
confirm = await ctx.prompt(msg, timeout=120.0, delete_after=True)
440+
if confirm:
441+
await self.pool.execute(query, prefix, ctx.guild.id)
442+
get_prefix.cache_invalidate(self.bot, ctx.message)
443+
await ctx.send(f"The prefix `{prefix}` has been successfully deleted")
444+
elif confirm is None:
445+
await ctx.send("Confirmation timed out. Cancelled deletion...")
446+
else:
447+
await ctx.send("Confirmation cancelled. Please try again")
448+
337449

338450
async def setup(bot: Rodhaj) -> None:
339451
await bot.add_cog(Config(bot))

bot/libs/utils/prefix.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Union
4+
5+
import discord
6+
from async_lru import alru_cache
7+
8+
if TYPE_CHECKING:
9+
from bot.rodhaj import Rodhaj
10+
11+
12+
@alru_cache(maxsize=1024)
13+
async def get_prefix(bot: Rodhaj, message: discord.Message) -> Union[str, list[str]]:
14+
"""Obtains the prefix for the guild
15+
16+
This coroutine is heavily cached in order to reduce database calls
17+
and improved performance
18+
19+
20+
Args:
21+
bot (Rodhaj): An instance of `Rodhaj`
22+
message (discord.Message): The message that is processed
23+
24+
Returns:
25+
Union[str, List[str]]: The default prefix or
26+
a list of prefixes (including the default)
27+
"""
28+
user_id = bot.user.id # type: ignore
29+
30+
# By putting the base with the mentions, we are effectively
31+
# doing the exact same thing as commands.when_mentioned
32+
base = [f"<@!{user_id}> ", f"<@{user_id}> ", bot.default_prefix]
33+
if message.guild is None:
34+
get_prefix.cache_invalidate(bot, message)
35+
return base
36+
37+
query = """
38+
SELECT prefix
39+
FROM guild_config
40+
WHERE id = $1;
41+
"""
42+
prefixes = await bot.pool.fetchval(query, message.guild.id)
43+
if prefixes is None:
44+
get_prefix.cache_invalidate(bot, message)
45+
return base
46+
base.extend(item for item in prefixes)
47+
return base

bot/rodhaj.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
send_error_embed,
2121
)
2222
from libs.utils.config import RodhajConfig
23+
from libs.utils.prefix import get_prefix
2324
from libs.utils.reloader import Reloader
2425

2526
if TYPE_CHECKING:
@@ -45,13 +46,14 @@ def __init__(
4546
allowed_mentions=discord.AllowedMentions(
4647
everyone=False, replied_user=False
4748
),
48-
command_prefix=["r>", "?", "!"],
49+
command_prefix=get_prefix,
4950
help_command=RodhajHelp(),
5051
intents=intents,
5152
tree_cls=RodhajCommandTree,
5253
*args,
5354
**kwargs,
5455
)
56+
self.default_prefix = "r>"
5557
self.logger = logging.getLogger("rodhaj")
5658
self.session = session
5759
self.partial_config: Optional[PartialConfig] = None

0 commit comments

Comments
 (0)