async def put_doc( name: str, original_msg: discord.Message, msg_invoker: discord.Member, page: int = 0 ): """ Helper function to get docs """ module_objs, main_embeds = await put_main_doc(name, original_msg) if module_objs is None or main_embeds is None: return allowed_obj_names = { "Modules": [], "Types": [], "Functions": [], "Methods": [], } formatted_obj_names = { "module": "Modules", "type": "Types", "function": "Functions", "method_descriptor": "Methods", } for oname, modmember in module_objs.items(): if type(modmember).__name__ == "builtin_function_or_method": # Disambiguate into funtion or method obj_type_name = None if isinstance(modmember, types.BuiltinFunctionType): obj_type_name = "Functions" elif isinstance(modmember, types.BuiltinMethodType): obj_type_name = "Methods" else: obj_type_name = formatted_obj_names.get(type(modmember).__name__) if oname.startswith("__") or obj_type_name is None: continue allowed_obj_names[obj_type_name].append(oname) embeds = [] for otype, olist in allowed_obj_names.items(): if not olist: continue embeds.append( embed_utils.create( title=f"{otype} in `{name}`", description=utils.code_block("\n".join(olist)), ) ) main_embeds.extend(embeds) page_embed = embed_utils.PagedEmbed( original_msg, main_embeds, msg_invoker, f"doc {name}", page ) await page_embed.mainloop()
async def cmd_vibecheck(self): """ ->type Play With Me :snake: ->signature pg!vibecheck ->description Check my mood. ----- Implement pg!vibecheck, to check the snek's emotion """ async with db.DiscordDB("emotions") as db_obj: all_emotions = db_obj.get({}) emotion_percentage = vibecheck.get_emotion_percentage(all_emotions, round_by=-1) all_emotion_response = vibecheck.get_emotion_desc_dict(all_emotions) bot_emotion = max(emotion_percentage.keys(), key=lambda key: emotion_percentage[key]) msg = all_emotion_response[bot_emotion]["msg"] emoji_link = all_emotion_response[bot_emotion]["emoji_link"] if all_emotion_response[bot_emotion].get("override_emotion", None): bot_emotion = all_emotion_response[bot_emotion]["override_emotion"] color = pygame.Color(vibecheck.EMOTION_COLORS[bot_emotion]) t = time.time() pygame.image.save(vibecheck.emotion_pie_chart(all_emotions, 400), f"temp{t}.png") file = discord.File(f"temp{t}.png") try: await self.response_msg.delete() except discord.errors.NotFound: # Message already deleted pass embed_dict = { "title": f"The snek is {bot_emotion} right now!", "description": msg, "thumbnail_url": emoji_link, "footer_text": "This is currently in beta version, so the end product may look different", "footer_icon_url": "https://cdn.discordapp.com/emojis/844513909158969374.png?v=1", "image_url": f"attachment://temp{t}.png", "color": utils.color_to_rgb_int(color), } embed = embed_utils.create(**embed_dict) await self.invoke_msg.reply(file=file, embed=embed, mention_author=False) os.remove(f"temp{t}.png")
async def handle(invoke_msg: discord.Message, response_msg: discord.Message = None): """ Handle a pg! command posted by a user """ is_admin, is_priv = get_perms(invoke_msg.author) if is_admin and invoke_msg.content.startswith(f"{common.PREFIX}stop"): splits = invoke_msg.content.strip().split(" ") splits.pop(0) try: if splits: for uid in map(utils.filter_id, splits): if uid in common.TEST_USER_IDS: break else: return except ValueError: if response_msg is None: await embed_utils.send( invoke_msg.channel, title="Invalid arguments!", description= "All arguments must be integer IDs or member mentions", color=0xFF0000, ) else: await embed_utils.replace( response_msg, title="Invalid arguments!", description= "All arguments must be integer IDs or member mentions", color=0xFF0000, ) return if response_msg is None: await embed_utils.send( invoke_msg.channel, title="Stopping bot...", description="Change da world,\nMy final message,\nGoodbye.", ) else: await embed_utils.replace( response_msg, title="Stopping bot...", description="Change da world,\nMy final message,\nGoodbye.", ) sys.exit(0) if (common.TEST_MODE and common.TEST_USER_IDS and invoke_msg.author.id not in common.TEST_USER_IDS): return if response_msg is None: response_msg = await embed_utils.send( invoke_msg.channel, title="Your command is being processed:", fields=(("\u2800", "`Loading...`", False), ), ) if not common.TEST_MODE and not common.GENERIC: log_txt_file = None escaped_cmd_text = discord.utils.escape_markdown(invoke_msg.content) if len(escaped_cmd_text) > 2047: with io.StringIO(invoke_msg.content) as log_buffer: log_txt_file = discord.File(log_buffer, filename="command.txt") await common.log_channel.send( embed=embed_utils.create( title= f"Command invoked by {invoke_msg.author} / {invoke_msg.author.id}", description=escaped_cmd_text if len(escaped_cmd_text) <= 2047 else escaped_cmd_text[:2044] + "...", fields=(( "\u200b", f"by {invoke_msg.author.mention}\n**[View Original]({invoke_msg.jump_url})**", False, ), ), ), file=log_txt_file, ) cmd = (admin.AdminCommand(invoke_msg, response_msg) if is_admin else user.UserCommand(invoke_msg, response_msg)) cmd.is_priv = is_priv await cmd.handle_cmd() return response_msg
async def put_main_doc(name: str, original_msg: discord.Message): """ Put main part of the doc into embed(s) """ splits = name.split(".") try: is_builtin = bool(getattr(builtins, splits[0])) except AttributeError: is_builtin = False if splits[0] not in doc_module_dict and not is_builtin: await embed_utils.replace( original_msg, title="Unknown module!", description="No such module was found.", ) return None, None module_objs = dict(doc_module_dict) obj = None for part in splits: try: try: obj = getattr(builtins, part) except AttributeError: obj = module_objs[part] module_objs = {} for i in dir(obj): if i != "__abstractmethods__": module_objs[i] = getattr(obj, i) except KeyError: await embed_utils.replace( original_msg, title="Class/function/sub-module not found!", description=f"There's no such thing here named `{name}`", ) return None, None if isinstance(obj, (int, float, str, dict, list, tuple, bool)): await embed_utils.replace( original_msg, title=f"Documentation for `{name}`", description=f"{name} is a constant with a type of " f"`{obj.__class__.__name__}` which does not have documentation.", ) return None, None header = "" if splits[0] == "pygame": doclink = "https://www.pygame.org/docs" if len(splits) > 1: doclink += "/ref/" + splits[1].lower() + ".html" doclink += "#" doclink += "".join([s + "." for s in splits])[:-1] header = "Online documentation: " + doclink + "\n" docs = "" if obj.__doc__ is None else obj.__doc__ embeds = [] lastchar = 0 cnt = 0 while len(docs) >= lastchar: cnt += 1 if cnt >= common.DOC_EMBED_LIMIT: text = docs[lastchar:] else: text = docs[lastchar : lastchar + 2040] # Try to split docs into paragraphs. If that does not work, split # based on sentences. If that too does not work, then just split # blindly ind = text.rfind("\n\n") if ind > 1500: lastchar += ind text = text[:lastchar] else: ind = text.rfind("\n") if ind > 1500: lastchar += ind text = text[:lastchar] else: lastchar += 2040 if text: embeds.append( embed_utils.create( title=f"Documentation for `{name}`", description=header + utils.code_block(text), ) ) header = "" if cnt >= common.DOC_EMBED_LIMIT: break if not embeds: await embed_utils.replace( original_msg, title="Class/function/sub-module not found!", description=f"There's no such thing here named `{name}`", ) return None, None return module_objs, embeds
async def cmd_sudo_clone( self, *msgs: discord.Message, destination: Optional[common.Channel] = None, embeds: bool = True, attachments: bool = True, as_spoiler: bool = False, info: bool = False, author_info: bool = True, skip_empty: bool = True, ): """ ->type More admin commands ->signature pg!sudo clone <*messages> [destination=] [embeds=True] [attachments=True] [as_spoiler=False] [info=False] [author_info=True] ->description Clone a message through the bot ->extended description Clone the given messages and send them to the given destination channel. __Args__: `*messages: (Message)` > A sequence of discord messages whose text, contents, attachments or embeds should be cloned. `destination: (Channel) =` > A destination channel to send the cloned outputs to. > If omitted, the destination will be the channel where > this command was invoked. `as_attachment: (bool) = False` > Whether the text content (if present) of the given > messages should be sent as an attachment (`.txt`) > or as embed containing it inside a code block in its > description. `attachments: (bool) = True` > Whether the attachments of the given messages > should be cloned as well (if possible). `embeds: (bool) = True` > Whether the embeds of the given messages > should be cloned along with the outut messages. +===+ `as_spoiler: (bool) = False` > If set to `True`, the attachments of the input messages > will be explicitly marked as spoilers when sent to the > destination channel. `info: (bool) = False` > If set to `True`, an embed containing info > about each message will be sent along with > the message data output. `author_info: (bool) = True` > If set to `True`, extra information about > the message authors will be added to the > info embed which is sent if `info` is set > to `True`. `skip_empty: (bool) = True` > Whether empty messages > should be skipped. __Returns__: > One or more cloned messages with attachents > or embeds based on the given input. __Raises__: > `BotException`: One or more given arguments are invalid. > `HTTPException`: An invalid operation was blocked by Discord. ----- """ if not isinstance(destination, discord.TextChannel): destination = self.channel if not utils.check_channel_permissions( self.author, destination, permissions=("view_channel", "send_messages") ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) checked_channels = set() for i, msg in enumerate(msgs): if msg.channel not in checked_channels: if not utils.check_channel_permissions( self.author, msg.channel, permissions=("view_channel",) ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) else: checked_channels.add(msg.channel) if not i % 50: await asyncio.sleep(0) if not msgs: raise BotException( "Invalid arguments!", "No messages given as input.", ) load_embed = embed_utils.create( title="Your command is being processed:", fields=(("\u2800", "`...`", False),), ) msg_count = len(msgs) no_mentions = discord.AllowedMentions.none() for i, msg in enumerate(msgs): if msg_count > 2 and not i % 3: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Messages", value=f"`{i}/{msg_count}` messages processed\n" f"{(i/msg_count)*100:.01f}% | " + utils.progress_bar(i / msg_count, divisions=30), ), 0, ) await destination.trigger_typing() cloned_msg0 = None attached_files = [] if msg.attachments and attachments: with io.StringIO("This file was too large to be cloned.") as fobj: attached_files = [ ( await a.to_file(spoiler=a.is_spoiler() or as_spoiler) if a.size <= self.filesize_limit else discord.File(fobj, f"filetoolarge - {a.filename}.txt") ) for a in msg.attachments ] if msg.content or msg.embeds or attached_files: if len(msg.content) > 2000: start_idx = 0 stop_idx = 0 for i in range(len(msg.content) // 2000): start_idx = 2000 * i stop_idx = 2000 + 2000 * i if not i: cloned_msg0 = await destination.send( content=msg.content[start_idx:stop_idx], allowed_mentions=no_mentions, ) else: await destination.send( content=msg.content[start_idx:stop_idx], allowed_mentions=no_mentions, ) with io.StringIO(msg.content) as fobj: await destination.send( content=msg.content[stop_idx:], embed=embed_utils.create(footer_text="Full message data"), file=discord.File(fobj, filename="messagedata.txt"), allowed_mentions=no_mentions, ) await destination.send( embed=msg.embeds[0] if msg.embeds and embeds else None, file=attached_files[0] if attached_files else None, ) else: cloned_msg0 = await destination.send( content=msg.content, embed=msg.embeds[0] if msg.embeds and embeds else None, file=attached_files[0] if attached_files else None, allowed_mentions=no_mentions, ) elif not skip_empty: raise BotException("Cannot clone an empty message!", "") for i in range(1, len(attached_files)): await self.channel.send( file=attached_files[i], ) for i in range(1, len(msg.embeds)): await self.channel.send( embed=msg.embeds[i], ) if info: await self.channel.send( embed=embed_utils.get_msg_info_embed(msg, author=author_info), reference=cloned_msg0, ) await asyncio.sleep(0) if msg_count > 2: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Completed", value=f"`{msg_count}/{msg_count}` messages processed\n" "100% | " + utils.progress_bar(1.0, divisions=30), ), 0, ) try: await self.response_msg.delete(delay=10 if msg_count > 2 else 0) except discord.NotFound: pass
async def cmd_sudo_get( self, *msgs: discord.Message, destination: Optional[common.Channel] = None, as_attachment: bool = False, attachments: bool = True, embeds: bool = True, info: bool = False, author_info: bool = True, ): """ ->type More admin commands ->signature pg!sudo_get <*messages> [destination=] [as_attachment=False] [attachments=True] [embeds=True] [info=False] [author_info=False] ->description Get the text of messages through the bot ->extended description Get the contents, attachments and serialized embeds of the given messages and send them to the given destination channel. __Args__: `*messages: (Message)` > A sequence of discord messages whose text, contents, attachments or embeds should be retrieved. `destination: (Channel) =` > A destination channel to send the generated outputs to. > If omitted, the destination will be the channel where > this command was invoked. `as_attachment: (bool) = False` > Whether the text content (if present) of the given > messages should be sent as an attachment (`.txt`) > or as embed containing it inside a code block in its > description. This will always occur if the text > content is above 2000 characters. `attachments: (bool) = True` > Whether the attachments of the given messages > should be retrieved (when possible). `embeds: (bool) = True` > Whether the embeds of the given messages > should be retrieved (as serialized JSON data). +===+ `info: (bool) = False` > If set to `True`, an embed containing info > about each message will be sent along with > the message data output. `author_info: (bool) = True` > If set to `True`, extra information about > the message authors will be added to the > info embed which is sent if `info` is set > to `True`. __Returns__: > One or more messages with attachents or embeds > based on the given input. __Raises__: > `BotException`: One or more given arguments are invalid. > `HTTPException`: An invalid operation was blocked by Discord. ----- """ if not isinstance(destination, discord.TextChannel): destination = self.channel if not utils.check_channel_permissions( self.author, destination, permissions=("view_channel", "send_messages") ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) checked_channels = set() for i, msg in enumerate(msgs): if msg.channel not in checked_channels: if not utils.check_channel_permissions( self.author, msg.channel, permissions=("view_channel",) ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) else: checked_channels.add(msg.channel) if not i % 50: await asyncio.sleep(0) if not msgs: raise BotException( "Invalid arguments!", "No messages given as input.", ) load_embed = embed_utils.create( title="Your command is being processed:", fields=(("\u2800", "`...`", False),), ) msg_count = len(msgs) for i, msg in enumerate(msgs): if msg_count > 2 and not i % 3: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Messages", value=f"`{i}/{msg_count}` messages processed\n" f"{(i/msg_count)*100:.01f}% | " + utils.progress_bar(i / msg_count, divisions=30), ), 0, ) await destination.trigger_typing() escaped_msg_content = msg.content.replace("```", "\\`\\`\\`") attached_files = None if attachments: with io.StringIO("This file was too large to be duplicated.") as fobj: attached_files = [ ( await a.to_file(spoiler=a.is_spoiler()) if a.size <= self.filesize_limit else discord.File(fobj, f"filetoolarge - {a.filename}.txt") ) for a in msg.attachments ] if info: info_embed = embed_utils.get_msg_info_embed(msg, author_info) info_embed.set_author(name="Message data & info") info_embed.title = "" info_embed.description = "".join( ( "__Text" + (" (Shortened)" if len(escaped_msg_content) > 2000 else "") + "__:", f"\n\n ```\n{escaped_msg_content[:2001]}\n\n[...]\n```" + "\n\u2800" if len(escaped_msg_content) > 2000 else "\n\u2800", ) ) content_file = None if as_attachment or len(msg.content) > 2000: with io.StringIO(msg.content) as fobj: content_file = discord.File(fobj, "messagedata.txt") await destination.send(embed=info_embed, file=content_file) elif as_attachment: with io.StringIO(msg.content) as fobj: await destination.send( file=discord.File(fobj, "messagedata.txt"), embed=embed_utils.create( author_name="Message data", description=f"**[View Original Message]({msg.jump_url})**", ), ) else: if len(msg.content) > 2000 or len(escaped_msg_content) > 2000: with io.StringIO(msg.content) as fobj: await destination.send( file=discord.File(fobj, "messagedata.txt"), embed=embed_utils.create( author_name="Message data", description=f"**[View Original Message]({msg.jump_url})**", ), ) else: await embed_utils.send( self.channel, author_name="Message data", description="```\n{0}```".format(escaped_msg_content), fields=( ( "\u2800", f"**[View Original Message]({msg.jump_url})**", False, ), ), ) if attached_files: for i in range(len(attached_files)): await self.channel.send( content=f"**Message attachment** ({i+1}):", file=attached_files[i], ) if embeds and msg.embeds: embed_data_fobjs = [] for embed in msg.embeds: embed_data_fobj = io.StringIO() embed_utils.export_embed_data( embed.to_dict(), fp=embed_data_fobj, indent=4, as_json=True, ) embed_data_fobj.seek(0) embed_data_fobjs.append(embed_data_fobj) for i in range(len(embed_data_fobjs)): await self.channel.send( content=f"**Message embed** ({i+1}):", file=discord.File( embed_data_fobjs[i], filename="embeddata.json" ), ) for embed_data_fobj in embed_data_fobjs: embed_data_fobj.close() await asyncio.sleep(0) if msg_count > 2: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Completed", value=f"`{msg_count}/{msg_count}` messages processed\n" "100% | " + utils.progress_bar(1.0, divisions=30), ), 0, ) try: await self.response_msg.delete(delay=10 if msg_count > 2 else 0) except discord.NotFound: pass
async def cmd_sudo( self, *datas: Union[discord.Message, String], destination: Optional[common.Channel] = None, from_attachment: bool = True, mention: bool = False, ): """ ->type More admin commands ->signature pg!sudo <*datas> [destination=] [from_attachment=True] ->description Send a message through the bot ->extended description Send a sequence of messages contain text from the given data using the specified arguments. __Args__: `*datas: (Message|String)` > A sequence of discord messages whose text > or text attachment should be used as input, > or strings. `destination (TextChannel) = ` > A destination channel to send the output to. `from_attachment (bool) = True` > Whether the attachment of an input message should be > used to create a message. `mention (bool) = False` > Whether any mentions in the given input text > should ping their target. If set to `True`, > any role/user/member that the bot is allowed to ping will > be pinged. __Returns__: > One or more generated messages based on the given input. __Raises__: > `BotException`: One or more given arguments are invalid. > `HTTPException`: An invalid operation was blocked by Discord. ->example command pg!sudo "lol" "that" "was" "funny /s" destination=#general pg!sudo 987654321987654321 "Additionally, ..." 123456739242423 from_attachment=True ----- Implement pg!sudo, for admins to send messages via the bot """ if destination is None: destination = self.channel if not utils.check_channel_permissions( self.author, destination, permissions=("view_channel", "send_messages") ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) for i, data in enumerate(datas): if isinstance(data, discord.Message): if not utils.check_channel_permissions( self.author, data.channel, permissions=("view_channel",), ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) if not i % 50: await asyncio.sleep(0) output_strings = [] load_embed = embed_utils.create( title="Your command is being processed:", fields=( ("\u2800", "`...`", False), ("\u2800", "`...`", False), ), ) data_count = len(datas) for i, data in enumerate(datas): if data_count > 2 and not i % 3: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Inputs", value=f"`{i}/{data_count}` inputs processed\n" f"{(i/data_count)*100:.01f}% | " + utils.progress_bar(i / data_count, divisions=30), ), 0, ) attachment_msg = None if isinstance(data, String): if not data.string: attachment_msg = self.invoke_msg else: msg_text = data.string output_strings.append(msg_text) elif isinstance(data, discord.Message): if not utils.check_channel_permissions( self.author, data.channel, permissions=("view_channel",), ): raise BotException( "Not enough permissions", "You do not have enough permissions to run this command with the specified arguments.", ) if from_attachment: attachment_msg = data else: src_msg_txt = data.content if src_msg_txt: output_strings.append(src_msg_txt) else: raise BotException( f"Input {i}: No message text found!", "The message given as input does not have any text content.", ) if attachment_msg: if not attachment_msg.attachments: raise BotException( f"Input {i}: No valid attachment found in message.", "It must be a `.txt` file containing text data." " If you want to retrieve the content of the" " given message(s) instead, set the" "` from_attachment=` argument to `False`", ) for attachment in attachment_msg.attachments: if ( attachment.content_type is not None and attachment.content_type.startswith("text") ): attachment_obj = attachment break else: raise BotException( f"Input {i}: No valid attachment found in message.", "It must be a `.txt` file containing text data.", ) msg_text = await attachment_obj.read() msg_text = msg_text.decode() if 0 < len(msg_text) <= 2000: output_strings.append(msg_text) else: raise BotException( f"Input {i}: Too little/many characters!", "a Discord message must contain at least one character and cannot contain more than 2000.", ) await asyncio.sleep(0) if not datas: data_count = 1 attachment_msg = self.invoke_msg if not attachment_msg.attachments: raise BotException( "No valid attachment found in message.", "It must be a `.txt` file containing text data.", ) for attachment in attachment_msg.attachments: if ( attachment.content_type is not None and attachment.content_type.startswith("text") ): attachment_obj = attachment break else: raise BotException( "No valid attachment found in message.", "It must be a `.txt` file containing text data.", ) msg_text = await attachment_obj.read() msg_text = msg_text.decode() if 0 < len(msg_text) <= 2000: output_strings.append(msg_text) else: raise BotException( "Too little/many characters!", "a Discord message must contain at least one character and cannot contain more than 2000.", ) if data_count > 2: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Processing Completed", value=f"`{data_count}/{data_count}` inputs processed\n" "100% | " + utils.progress_bar(1.0, divisions=30), ), 0, ) allowed_mentions = ( discord.AllowedMentions.all() if mention else discord.AllowedMentions.none() ) output_count = len(output_strings) for j, msg_txt in enumerate(output_strings): if output_count > 2 and not j % 3: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Creating Messages", value=f"`{j}/{output_count}` messages created\n" f"{(j/output_count)*100:.01f}% | " + utils.progress_bar(j / output_count, divisions=30), ), 1, ) await destination.send(content=msg_txt, allowed_mentions=allowed_mentions) if data_count > 2: await embed_utils.edit_field_from_dict( self.response_msg, load_embed, dict( name="Creation Completed", value=f"`{output_count}/{output_count}` messages created\n" "100% | " + utils.progress_bar(1.0, divisions=30), ), 1, ) try: await self.invoke_msg.delete() await self.response_msg.delete(delay=10.0 if data_count > 2 else 0.0) except discord.NotFound: pass
async def send_help_message( original_msg: discord.Message, invoker: discord.Member, commands: tuple[str, ...], cmds_and_funcs: dict[str, typing.Callable], groups: dict[str, list], page: int = 0, ): """ Edit original_msg to a help message. If command is supplied it will only show information about that specific command. Otherwise sends the general help embed. Args: original_msg: The message to edit invoker: The member who requested the help command commands: A tuple of command names passed by user for help. cmds_and_funcs: The name-function pairs to get the docstrings from groups: The name-list pairs of group commands page: The page of the embed, 0 by default """ doc_fields = {} embeds = [] if not commands: functions = {} for key, func in cmds_and_funcs.items(): if hasattr(func, "groupname"): key = f"{func.groupname} {' '.join(func.subcmds)}" functions[key] = func for func in functions.values(): data = get_doc_from_func(func) if not data: continue if not doc_fields.get(data["type"]): doc_fields[data["type"]] = ["", "", True] doc_fields[data["type"]][0] += f"{data['signature'][2:]}\n" doc_fields[data["type"]][1] += (f"`{data['signature']}`\n" f"{data['description']}\n\n") doc_fields_cpy = doc_fields.copy() for doc_field_name in doc_fields: doc_field_list = doc_fields[doc_field_name] doc_field_list[ 1] = f"```\n{doc_field_list[0]}\n```\n\n{doc_field_list[1]}" doc_field_list[0] = f"__**{doc_field_name}**__" doc_fields = doc_fields_cpy embeds.append( discord.Embed( title=common.BOT_HELP_PROMPT["title"], description=common.BOT_HELP_PROMPT["description"], color=common.BOT_HELP_PROMPT["color"], )) for doc_field in list(doc_fields.values()): body = f"{doc_field[0]}\n\n{doc_field[1]}" embeds.append( embed_utils.create( title=common.BOT_HELP_PROMPT["title"], description=body, color=common.BOT_HELP_PROMPT["color"], )) elif commands[0] in cmds_and_funcs: func_name = commands[0] funcs = groups[func_name] if func_name in groups else [] funcs.insert(0, cmds_and_funcs[func_name]) for func in funcs: if (commands[1:] and commands[1:] != getattr(func, "subcmds", ())[:len(commands[1:])]): continue doc = get_doc_from_func(func) if not doc: # function found, but does not have help. return await embed_utils.replace( original_msg, title="Could not get docs", description="Command has no documentation", color=0xFF0000, ) body = f"`{doc['signature']}`\n`Category: {doc['type']}`\n\n" desc = doc["description"] ext_desc = doc.get("extended description") if ext_desc: desc = f"> *{desc}*\n\n{ext_desc}" desc_list = desc.split(sep="+===+") body += f"**Description:**\n{desc_list[0]}" embed_fields = [] example_cmd = doc.get("example command") if example_cmd: embed_fields.append(["Example command(s):", example_cmd, True]) if len(desc_list) == 1: embeds.append( embed_utils.create( title=f"Help for `{func_name}`", description=body, color=common.BOT_HELP_PROMPT["color"], fields=embed_fields, )) else: embeds.append( embed_utils.create( title=f"Help for `{func_name}`", description=body, color=common.BOT_HELP_PROMPT["color"], )) desc_list_len = len(desc_list) for i in range(1, desc_list_len): embeds.append( embed_utils.create( title=f"Help for `{func_name}`", description=desc_list[i], color=common.BOT_HELP_PROMPT["color"], fields=embed_fields if i == desc_list_len - 1 else (), )) if not embeds: return await embed_utils.replace( original_msg, title="Command not found", description="No such command exists", color=0xFF0000, ) await embed_utils.PagedEmbed(original_msg, embeds, invoker, f"help {' '.join(commands)}", page).mainloop()