Discord Name: Vertyco#0117
GitHub Repository (Must be V3): GitHub - vertyco/vrt-cogs: Utility cogs for Red by Vertyco
Description: Assorted utility cogs for game servers that I have created over the past couple months while learning python.
Discord Name: Vertyco#0117
GitHub Repository (Must be V3): GitHub - vertyco/vrt-cogs: Utility cogs for Red by Vertyco
Description: Assorted utility cogs for game servers that I have created over the past couple months while learning python.
Bumping since its been a full year now, all my cogs will be ready for dpy2 when red hits 3.5
Since 3.5 will break every cog that was not specifically coded with it in mind, you need to ensure your repo is updated to be compatible with 3.5 prior to a review being possible. The relevant documentation you will need to update your repo is the following:
-discord.py 2.0 migration: https://discordpy.readthedocs.io/en/v2.2.3/migrating.html
-Red 3.5 breakages: https://docs.discord.red/en/stable/incompatible_changes/3.5.html#incompatible-changes-3-5
Once your repo is updated to support 3.5 on the default branch, please make a comment in this thread to confirm you are once again ready for review. We will only be reviewing repos which comment to confirm 3.5 support, but we will prioritize repos that have been in the queue for a longer period of time first.
If you wish to revoke your application, please comment accordingly so we can clean up the thread. If you need time to make changes, but are actively working on making updates, please also comment accordingly. Applications which do not see any activity within 1 month from today will be closed.
Hi Flame, can confirm my repo already supports 3.5
Hi Vertyco, thank you for your patience while I review your repo.
Commit hash at time of review - 6da8916343af1034f69a56b13f9b5f7a16ae94f8
Some general comments regarding the whole repo:
There’s a lot of missing permissions checks for both the bot and the user. These are the main gotcha’s cog creators can run into especially regarding role permissions when assigning roles to users. Some other general things I noticed is using @commands.admin()
for everything which isn’t the best tracker for permissions checks. This decorator only checks that the admin
role has been setup by the server owner (or bot owner). This may not reliably translate trust to the user and can be a deterrant for some bot owners who aren’t intimately aware of the bot working like this.
I imagine many people might install your cogs and not have setup the admin role and suddenly none of your commands work. While that’s totally up to you to decide there’s at least one instance I found where this can lead to potential hierarchy escalation which is not okay. If you intended to check that the user has administrator permission then you should use @commands.admin_or_permissions(administrator=True)
. Then the server owner can set an explicit admin role or anyone who has the administrator permission can also use this. No two people have the same interpretation of a servers Admin and Mod definition and blanket assumptions tend to be bad. If you want to opt out of Red’s mod and admin role checks entirely look at using @commands.has_permissions()
then only people with that explicit role/channel permission can run the command. Instances where this isn’t a problem are labeled as optional.
For simplicity I tend to use manage_messages
as a base trust permission as generally servers treat manage messages permission as the most basic moderator signal. If you can’t delete someone elses message you probably shouldn’t be modifying the servers bot settings. Again this is my opinion on the matter and you’re free to set things up as you wish.
I understand that these cogs have been in flux since prior to 3.5’s release and you have attempted some backwards comapatibility but the way you went about that backwards compatibility has some potential issues and have been noted below.
Lines 177-246: If the bot does not have embed_links
permission and there is a response larger than 2000 characters all of this will fail.
Line 369: You should not allow user defined regex to be unregulated like this. A server admin could define a regex pattern that is catastrophically backtracking. This would mean any time someone calls the API in that server the bot could indefinitely hang for everyone. There are ways to avoid this issue namely the re2 module from facebook is designed to prevent this but that is complicated to install. Another approach might be to spin up the regex searching in a process pool and let a whole new python interpreter run the regex and if it takes too long spit it out. An example of this failing would be ^(?:a+)+$/
and someone sending a message with about 100 a’s. This pattern will compile correctly according to your original checks but would cause the bot to hang attempting to replace the a’s on this pattern. For an example of the process pool in action you can see my ReTrigger cog. I have had marginal success using the regex library as well specifically not allowing this particular pattern to run at all and returning nothing.
Lines 122-131: Some bot owners might not appreciate the error message being so open even when they run it. It would be better to make the upload/traceback error optional for the owner asking them if they want it there specifically. The bot already has [p]traceback
command which the owner can use to pull this information publicly if wanted.
Lines 199-258: There should be some sort of opt-in setting for this. Not all the users in the channels being searched necessarily want their messages being fed to ChatGPT and could be considered in violation of discord developer TOS and the expected privacy. Users should explicitly opt into sending their data to a third party API. I know this seems a little prohibitive but the nature of the AI space is still relatively new and a lot of people don’t understand its rammifications just yet. There are definitely going to be people who don’t want their likeness used to train AI and allowing a bot owner to blanket send this data to a third party without their consent is problematic. If this was a locally hosted AI language model handling it it could be fine but as per the discord developer agreement bots should not perform actions for users without their consent. That especially includes passing their data to a third party without their consent.
Line 55, 155, 199, 711: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 298 and 371: It is not advised to respond with the full exception when a command errors, you should instead provide a nicer error message to the user especially for a blanket exception catch like this and you don’t know what exceptions can be thrown. It might be better to raise your own Errors from the API responses which then you can define the error response. If you want you can add an owner check and provide the full response to the owner then although some owners might prefer any exceptions appear in their logs privately instead of being sent in discord public fo anyone who can see the channel to witness and give anyone else a generic response that it failed and maybe why it failed without providing useless traceback information to the user.
Lines 906, 926, 944, 964: This is inaccurate, the bot could not have permission to attach_files
in the channel requested. You should check that prior to spending a bunch of time compressing the data and trying to send it again.
embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.attach_files
here.discord.__version__ < "2.0.0"
. Though I would personally recommend checking Red’s version usingfrom redbot.core import version_info, VersionInfo
async def setup(bot):
cog = ...
if version_info >= VersionInfo.from_str("3.5.0"):
await bot.add_cog(cog)
else:
bot.add_cog(cog)
Though you can also use the max_bot_version
key in info.json
to prevent users from upgrading to a version requiring the newer features as well.
Line 89, 129, 157, 254: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Lines 157 and 254: You should also be checking that the bot has attach_files
permission.
.close()
is only required on actual files read from disk so that other files can open and read them. Closing a buffer like this is not necessary since the garbage collector will automatically clean it up later. (Optional)Line 63-67: I don’t feel like fetching the message for every single reaction event is necessary here since you’re only using the message object to detect whether or not the message has existed somewhere if you’re receiving this event it clearly does or did. This consumes unnecessary rate limits in the channel for the bot especially on larger guilds with lots of potential reactions on messages. Since this is just tracking I don’t think these checks are worthwhile and can be eliminated entirely since you have the message ID already. The same can be considered for channel above but it does not affect the bots potential performance. (Optional)
Line 116, 139, 180: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 125: Typo? \b
is not a newline character. You might consider using from redbot.core.utils.chat_formatting import box
and then box(blacklist, lang="py")
instead. (Optional)
Line 32: See __init__.py for economytrack.
Line 141, 358, 799: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 339: You should be checking the bot has attach_files
permission in the channel.
Line 353: I recommend using @commands.admin_or_permissions()
here, not everyone who installs the bot knows to setup the admin role which would render every command unusable. At the very least I recommend setting a base required permission for the settings such as manage_messages
to signify someone who is most likely a mod or admin and then further customize requirements on additional commands. (Optional)
Line 13: See __init__.py for economytrack.
Line 70-72: Your typehint should be discord.Guild
but also if you intend these to be used in embeds a simpler way is to return the actual discord.Asset
instead of the url if it’s None. (Optional)
Line 61, 111, 148: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Lines 226 and 128: You should be checking that the bot has permission to embed_links
in the destination channel when being setup and when actually executing.
Lines 76 and 125: You should also be checking that the bot has permission to embed links in the channel not just assume it always will be from the settings.
Lines 175-184: You should probably check if the destination channel is already setup and whether or not the bot has permission to emebd links in there. (Optional as long as the correct permission checks are applied elsewhere.)
Line 138, 209, 249: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 116: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 121: You should be using config.all_users()
instead of this private method.
Line 38-46: See __init__.py for economytrack.
Line 258, 518, 562, 607, 778, 932, 1007, 1038, 1114, 1145: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Lines 225, 257, 716:, 912-920 You should be checking that the bot has attach_files
permission.
Lines 460, 488: Or the bot could be missing attach_files
permission.
Line 87: You might consider doing for gid in self.data.copy().keys()
since data is unused. (Optional)
Line 216: See __init__.py for economytrack.
Line 494: Typehint should be discord.Guild
. (Optional)
Line 677, 688, 699: These should have either prior checks for or try/except catches incase a role set is higher than the bots highest role or has been changed since originally setup.
Line 998: You can just set em.set_thumbnail(url=guild.icon)
and d.py will handle the conversion to string for you. (Optional)
Line 1066: Again Admin perm is okay but there should be some more granularity here. Not everyone sets the bots admin and furthermore that requires the server owner to setup. Consider adding a base level permission here. (Optional)
Line 2382: This one I cannot leave like this. Role changes of any sort need to check that the user in question actually has permission to manage roles lest they become open to role hierarchy issues. These group commands inherit the @commands.admin()
permission from lvl_group
but that is not enough of a check. On add_level_role
(line 2476) an Admin can use this to assign themselves or anyone a higher role than their own assuming the bot has a higher role than them which is possible. You should also be checking that the role in question is not higher than or equal to the bots highest role and the users highest role on top of the permissions checks as well as checking these when actually asigning the roles elsewhere. While these checks need to be in place for add_level_role
, on init_roles
, toggle_autoremove
, and del_level_roles
they’re not required as extensively but I highly recommend having at least @admin_or_permissions(manage_roles=True)
.
Lines 2411-2451: You might consider calculating the role changes first before adding/removing roles in this list to help reduce potential bugs where the settings might be configured incorrectly and this attempts to add roles that should be removed. (Optional)
Line 1073, 1335, 1625, 2186, 2385, 2669, 2712: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Lines 1372, 1382, and 2187: You should be checking that the bot has attach_files
permission.
Line 14: See __init__.py for economytrack.
Line 90, 110: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 72: See __init__.py for economytrack.
Lines 170, 180, and 199: These should be guarded against the bot not having permission to embed links and send messages to the log channel.
embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.Line 74, 153, 310, 436, 438, 446, 490, 584: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Line 153: You should be checking that the bot has attach_files
permission.
Line 56: This should be Optional[discord.Guild]
typehint. (Optional)
Lines56-69: You should consider not having this iterate over all of the bots guild and instead load the cache differently. (Optional)
attach_files
and embed_links
in the log channel.{}
because it doesn’t make sense to them and they know that that’s a formatted variable. (Optional)Line 124: This should say the bot requires manage permissions
enabled in the category. Manage roles is a guild permission not a channel permission.
Line 158: Consider using the discord.Message
converter here it can simplify some logic as it will allow message links and channel_id-message_id
pairs getting the exact message you require and removing the necessity of running the command in the channel required. (Optional)
Line 328: This is missing attach_files
permission check on the channel since you can sometimes attempt to upload a file.
Line 149: Setting ctx.assume_yes = True
here isn’t strictly necessary, it’s already set in [p]cog update
when you pass reload=True
to the command. It could also be problematic as you will want to avoid a user setting up a scheduling cog to automatically update and reload commands. I see that this isn’t currently prevented in core so I will be leaving this as optional. (Optional)
Line 261: You are aware that [p]pipinstall
is already part of core, right? I can’t think of many other pip commands one would want for their bot. (Optional)
Lines 522-525: redbot.core.utils.chat_formatting.text_to_file
exists for exactly this. (Optional)
Lines 522-525: You should be checking that the bot has permission to attach_files
.
Line 569: you should consider using discord.utils.utcnow()
to ensure you get a timezone aware datetime object or utilize datetime.datetime.now(datetime.timezone.utc)
. (Optional)
Line 232, 250, 403, 469, 531, 708, 856, 959: You should be checking that the bot has embed_links
permission with @commands.bot_has_permissions(embed_links=True)
to prevent errors running this command or alternatively provide a text only option in the body checking await ctx.embed_requested()
.
Lines 299-300: You should consider using red’s shared API token system for these particular credentials, though I understand not using them since this is a full OAuth requirement cog and you’ll want to store users credentials separately. (Optional)
Line 310: lol. This can potentially happen if another bot was scanning for tokens, or something and deleted the message before this bot had time to fully process it. (Optional)
Lines 922-926: While this will only happen if you run the command if you do run it on someone elses bot and the bot does not have attach_files
permission it will error.
Thank you for the review Trusty! All of the changes you have listed have been handled including about 95% of the optional ones. Commit hash for these changes is 3e7faf26fb22463e15a815daac7e5524db5ab623
Thanks again for taking the time to dig through my repo
Round 2. There’s a lot here that wasn’t present in my first review and some things that need to be addressed. Generally speaking we suggest avoiding use of globals() and global variables when we see them and that’s mainly what caught my eye here. Reading globals() is typically fine but writing to them we suggest avoiding. While investigating your usage of globals() in assistant I found some other problems.
While there’s no rules against making a cog accessible to other cogs there’s some things to be wary of. There’s plenty of tools at your disposal for getting references to other cogs via listeners, bot.get_cog, etc. Managing those should be done with care though. Storing a reference to a cog in your cog can cause some odd behavior such as, but not limited to below:
While you don’t use this reference for anything other than as a key in a dictionary, this also means that the garbage collector can’t clean it up properly when the cog is unloaded since it’s now referenced by your cog which now requires me to reload two cogs instead of just my cog.
There’s more discussion about this below. I am not particularly fond of the way you added third party support for assistant and I think it can be greatly improved in simplicity for both you and other cog creators.
Commit hash at time of review - 74f68cff613e0a12032e27b29b491bb1127199f5
install_msg
should explain that other cogs installed may register additional functionality on this cog.cog_unload
is still called on the old instance when unloaded but everything it had saved in memory is still active and available through assistant. @commands.Cog.listener()
async def on_assistant_cog_add(self, cog: commands.Cog):
# This function could also be a string, and can be async OR sync
schema = {
"cogname": "Fluent",
"name": "get_translation",
"description": "Use this function to translate text",
"parameters": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "the text to translate"},
"to_language": {
"type": "string",
"description": "the target language to translate to",
},
},
"required": ["message", "to_language"],
},
}
await cog.register_function(schema)
Then when assistant needs to find the result of the third party cog:
if conf.use_function_calls and extend_function_calls:
function_calls.extend(self.db.get_function_calls(conf))
function_map.update(self.db.get_function_map())
for cog_functions in self.registry.values():
for function_name, function in cog_functions.items():
cog = self.bot.get_cog(function.jsonschema.cogname)
if cog is None:
continue
func = getattr(cog, function_name, None)
if func is None:
continue
function_calls.append(function.jsonschema)
function_map[function_name] = func
This avoids overwriting functions in utils.py’s globals() and will get the currently listed method from the cog. This way that cog’s method isn’t still stored in memory when that cog is unloaded.
This way of handling registration also reduces code duplication. For example let’s say I already have a method that returns data in a way chatGPT might expect, I don’t want to have to duplicate the code when I can just tell assistant to call my existing function. If I want to provide a cleaner response it would be better for me to do something like:
async def get_translation_for_assistant(self, message: str, to_language: str):
try:
return await self.get_translation(message, to_language)
except SomeException:
return "There was an error getting a translation"
Letting me define a cleaner error message and not have to worry about anything like checking if my own cog is loaded when this function is present.
If it was done this way to pass attributes more easily that can still be done. You setup the ability to define the parameters, and self
is automatically when called on an instance of the cog. If the method requires ctx
you can pass that still too just have it defined in the schemas parameters and maybe special case it in assistant.
The only thing your method supports that my suggestion doesn’t is module level methods, so the method has to be part of the cog class (e.g. staticmethods). I don’t see any reason you should have to support module methods for this.
So for the Assistant cog storing other cog functions, the cog does not store the instance of the cog as a key, but rather the name of the cog itself as a string. cog = cog.qualified_name
this appears to already be in the hash that you reviewed so i may not be understanding what you meant, but Ive ensured that unloaded cog functions don’t remain registered with the cog_remove listener. Additionally i have added a bit in the cleanup method to remove any cogs that have been unloaded that somehow weren’t caught in the cog_remove listener.
install_msg
has been updatedI’ve added more robust checking to make sure the cog is loaded before mapping the function for the api call, however I don’t believe passing the function object itself is “bad” if the cleanup process is sufficient, and implementing the change you proposed would be a breaking change, so i’ve opted to keep the ability to pass functions in the code for now.
Let me know if there’s anything I missed
I’ve split the terminology a bit to better describe
[p]customfunc
menu with a string[p]customfunc
menu but it will say if its managed by a 3rd party cogThe register function now looks like this
async def register_function(self, cog_name: str, schema: dict) -> bool:
...
likewise with the rest of them.
The self.registry
is now just Dict[str, Dict[str, dict]]
or
{cog_name: {function_name: {function_json_schema}}}
@commands.Cog.listener()
async def on_assistant_cog_add(self, cog: commands.Cog):
"""Registers a command with Assistant enabling it to access translations"""
schema = {
"name": "get_translation",
"description": "Translate text to another language",
"parameters": {
"type": "object",
"properties": {
"message": {"type": "string", "description": "the text to translate"},
"to_language": {
"type": "string",
"description": "the target language to translate to",
},
},
"required": ["message", "to_language"],
},
}
await cog.register_function(cog_name="Fluent", schema=schema)
async def get_translation(self, message: str, to_language: str, *args, **kwargs) -> str:
lang = await self.converter(to_language)
if not lang:
return "Invalid target language"
try:
translation = await self.translate(message, lang)
return f"{translation.text}\n({translation.src} -> {lang})"
except Exception as e:
return f"Error: {e}"
The cog instance and function are fetched each time now to ensure nothing is stale.
Documentation has also been updated: vrt-cogs/assistant/example-funcs at main · vertyco/vrt-cogs · GitHub
Hi Vertyco, thank you for making those changes. I feel a lot better knowing that there’s no risk of assistant holding another cog’s methods hostage because of some race condition. I hope at the very least you’ve learned a lot through this process. Marking this as approved!