From e374c00e2a846cfd9f8c5468b5c2dab599c1f1e2 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 14 Aug 2020 21:09:51 +0100 Subject: [PATCH 1/8] Add constants for badges --- bot/constants.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index d01dcb0fcf..f3db802792 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -268,6 +268,17 @@ class Emojis(metaclass=YAMLGetter): status_idle: str status_dnd: str + badge_staff: str + badge_partner: str + badge_hypesquad: str + badge_bug_hunter: str + badge_hypesquad_bravery: str + badge_hypesquad_brilliance: str + badge_hypesquad_balance: str + badge_early_supporter: str + badge_bug_hunter_level_2: str + badge_verified_bot_developer: str + incident_actioned: str incident_unactioned: str incident_investigating: str From f87db13ee549c64723086948b101292da93934d8 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 14 Aug 2020 21:13:05 +0100 Subject: [PATCH 2/8] Add YAML values for badges --- config-default.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config-default.yml b/config-default.yml index e3ba9fb057..8c0092e767 100644 --- a/config-default.yml +++ b/config-default.yml @@ -38,6 +38,17 @@ style: status_dnd: "<:status_dnd:470326272082313216>" status_offline: "<:status_offline:470326266537705472>" + badge_staff: "<:discord_staff:743882896498098226>" + badge_partner: "<:partner:743882897131569323>" + badge_hypesquad: "<:hypesquad_events:743882896892362873>" + badge_bug_hunter: "<:bug_hunter_lvl1:743882896372269137>" + badge_hypesquad_bravery: "<:hypesquad_bravery:743882896745693335>" + badge_hypesquad_brilliance: "<:hypesquad_brilliance:743882896938631248>" + badge_hypesquad_balance: "<:hypesquad_balance:743882896460480625>" + badge_early_supporter: "<:early_supporter:743882896909140058>" + badge_bug_hunter_level_2: "<:bug_hunter_lvl2:743882896611344505>" + badge_verified_bot_developer: "<:verified_bot_dev:743882897299210310>" + incident_actioned: "<:incident_actioned:719645530128646266>" incident_unactioned: "<:incident_unactioned:719645583245180960>" incident_investigating: "<:incident_investigating:719645658671480924>" From ab133d914e21a298ddd743db578e7d4a2e33120c Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 14 Aug 2020 21:14:20 +0100 Subject: [PATCH 3/8] Add badges & status to user command --- bot/cogs/information.py | 95 +++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 28 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 8982196d1a..34a85a86b3 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -4,7 +4,7 @@ import textwrap from collections import Counter, defaultdict from string import Template -from typing import Any, Mapping, Optional, Union +from typing import Any, Mapping, Optional, Tuple, Union from discord import ChannelType, Colour, Embed, Guild, Member, Message, Role, Status, utils from discord.abc import GuildChannel @@ -184,6 +184,18 @@ async def server_info(self, ctx: Context) -> None: await ctx.send(embed=embed) + @staticmethod + def status_to_emoji(status: Status) -> str: + """Convert a Discord status into the relevant emoji.""" + if status is Status.offline: + return constants.Emojis.status_offline + elif status is Status.dnd: + return constants.Emojis.status_dnd + elif status is Status.idle: + return constants.Emojis.status_idle + else: + return constants.Emojis.status_online + @command(name="user", aliases=["user_info", "member", "member_info"]) async def user_info(self, ctx: Context, user: Member = None) -> None: """Returns info about a user.""" @@ -223,41 +235,68 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: if user.nick: name = f"{user.nick} ({name})" + badges = "" + + for badge, is_set in user.public_flags: + if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}")): + badges += emoji + " " + joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - description = [ - textwrap.dedent(f""" - **User Information** - Created: {created} - Profile: {user.mention} - ID: {user.id} - {custom_status} - **Member Information** - Joined: {joined} - Roles: {roles or None} - """).strip() + desktop_status = self.status_to_emoji(user.desktop_status) + web_status = self.status_to_emoji(user.web_status) + mobile_status = self.status_to_emoji(user.mobile_status) + + fields = [ + ( + "User information", + textwrap.dedent(f""" + Created: {created} + Profile: {user.mention} + ID: {user.id} + {custom_status} + """).strip() + ), + ( + "Member information", + textwrap.dedent(f""" + Joined: {joined} + Roles: {roles or None} + """).strip() + ), + ( + "Status", + textwrap.dedent(f""" + Desktop: {desktop_status} + Web: {web_status} + Mobile: {mobile_status} + """).strip() + ) ] # Show more verbose output in moderation channels for infractions and nominations if ctx.channel.id in constants.MODERATION_CHANNELS: - description.append(await self.expanded_user_infraction_counts(user)) - description.append(await self.user_nomination_counts(user)) + fields.append(await self.expanded_user_infraction_counts(user)) + fields.append(await self.user_nomination_counts(user)) else: - description.append(await self.basic_user_infraction_counts(user)) + fields.append(await self.basic_user_infraction_counts(user)) # Let's build the embed now embed = Embed( title=name, - description="\n\n".join(description) + description=badges ) + for field_name, field_content in fields: + embed.add_field(name=field_name, value=field_content, inline=False) + embed.set_thumbnail(url=user.avatar_url_as(static_format="png")) embed.colour = user.top_role.colour if roles else Colour.blurple() return embed - async def basic_user_infraction_counts(self, member: Member) -> str: + async def basic_user_infraction_counts(self, member: Member) -> Tuple[str, str]: """Gets the total and active infraction counts for the given `member`.""" infractions = await self.bot.api_client.get( 'bot/infractions', @@ -270,11 +309,11 @@ async def basic_user_infraction_counts(self, member: Member) -> str: total_infractions = len(infractions) active_infractions = sum(infraction['active'] for infraction in infractions) - infraction_output = f"**Infractions**\nTotal: {total_infractions}\nActive: {active_infractions}" + infraction_output = f"Total: {total_infractions}\nActive: {active_infractions}" - return infraction_output + return "Infractions", infraction_output - async def expanded_user_infraction_counts(self, member: Member) -> str: + async def expanded_user_infraction_counts(self, member: Member) -> Tuple[str, str]: """ Gets expanded infraction counts for the given `member`. @@ -288,9 +327,9 @@ async def expanded_user_infraction_counts(self, member: Member) -> str: } ) - infraction_output = ["**Infractions**"] + infraction_output = [] if not infractions: - infraction_output.append("This user has never received an infraction.") + infraction_output.append("No infractions") else: # Count infractions split by `type` and `active` status for this user infraction_types = set() @@ -313,9 +352,9 @@ async def expanded_user_infraction_counts(self, member: Member) -> str: infraction_output.append(line) - return "\n".join(infraction_output) + return "Infractions", "\n".join(infraction_output) - async def user_nomination_counts(self, member: Member) -> str: + async def user_nomination_counts(self, member: Member) -> Tuple[str, str]: """Gets the active and historical nomination counts for the given `member`.""" nominations = await self.bot.api_client.get( 'bot/nominations', @@ -324,21 +363,21 @@ async def user_nomination_counts(self, member: Member) -> str: } ) - output = ["**Nominations**"] + output = [] if not nominations: - output.append("This user has never been nominated.") + output.append("No nominations") else: count = len(nominations) is_currently_nominated = any(nomination["active"] for nomination in nominations) nomination_noun = "nomination" if count == 1 else "nominations" if is_currently_nominated: - output.append(f"This user is **currently** nominated ({count} {nomination_noun} in total).") + output.append(f"This user is **currently** nominated\n({count} {nomination_noun} in total)") else: output.append(f"This user has {count} historical {nomination_noun}, but is currently not nominated.") - return "\n".join(output) + return "Nominations", "\n".join(output) def format_fields(self, mapping: Mapping[str, Any], field_width: Optional[int] = None) -> str: """Format a mapping to be readable to a human.""" From ed4ebbf5f7ee751f87554831e277d270cf36ac40 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 14 Aug 2020 21:19:04 +0100 Subject: [PATCH 4/8] Update tests for user commands --- tests/bot/cogs/test_information.py | 87 +++++++++++++++++++----------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/tests/bot/cogs/test_information.py b/tests/bot/cogs/test_information.py index 79c0e0ad37..77b0ddf17a 100644 --- a/tests/bot/cogs/test_information.py +++ b/tests/bot/cogs/test_information.py @@ -215,10 +215,10 @@ def _method_subtests(self, method, test_values, default_header): with self.subTest(method=method, api_response=api_response, expected_lines=expected_lines): self.bot.api_client.get.return_value = api_response - expected_output = "\n".join(default_header + expected_lines) + expected_output = "\n".join(expected_lines) actual_output = asyncio.run(method(self.member)) - self.assertEqual(expected_output, actual_output) + self.assertEqual((default_header, expected_output), actual_output) def test_basic_user_infraction_counts_returns_correct_strings(self): """The method should correctly list both the total and active number of non-hidden infractions.""" @@ -249,7 +249,7 @@ def test_basic_user_infraction_counts_returns_correct_strings(self): }, ) - header = ["**Infractions**"] + header = "Infractions" self._method_subtests(self.cog.basic_user_infraction_counts, test_values, header) @@ -258,7 +258,7 @@ def test_expanded_user_infraction_counts_returns_correct_strings(self): test_values = ( { "api response": [], - "expected_lines": ["This user has never received an infraction."], + "expected_lines": ["No infractions"], }, # Shows non-hidden inactive infraction as expected { @@ -304,7 +304,7 @@ def test_expanded_user_infraction_counts_returns_correct_strings(self): }, ) - header = ["**Infractions**"] + header = "Infractions" self._method_subtests(self.cog.expanded_user_infraction_counts, test_values, header) @@ -313,15 +313,15 @@ def test_user_nomination_counts_returns_correct_strings(self): test_values = ( { "api response": [], - "expected_lines": ["This user has never been nominated."], + "expected_lines": ["No nominations"], }, { "api response": [{'active': True}], - "expected_lines": ["This user is **currently** nominated (1 nomination in total)."], + "expected_lines": ["This user is **currently** nominated", "(1 nomination in total)"], }, { "api response": [{'active': True}, {'active': False}], - "expected_lines": ["This user is **currently** nominated (2 nominations in total)."], + "expected_lines": ["This user is **currently** nominated", "(2 nominations in total)"], }, { "api response": [{'active': False}], @@ -334,7 +334,7 @@ def test_user_nomination_counts_returns_correct_strings(self): ) - header = ["**Nominations**"] + header = "Nominations" self._method_subtests(self.cog.user_nomination_counts, test_values, header) @@ -350,7 +350,10 @@ def setUp(self): self.bot.api_client.get = unittest.mock.AsyncMock() self.cog = information.Information(self.bot) - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_uses_string_representation_of_user_in_title_if_nick_is_not_available(self): """The embed should use the string representation of the user if they don't have a nick.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -362,7 +365,10 @@ def test_create_user_embed_uses_string_representation_of_user_in_title_if_nick_i self.assertEqual(embed.title, "Mr. Hemlock") - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_uses_nick_in_title_if_available(self): """The embed should use the nick if it's available.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -374,7 +380,10 @@ def test_create_user_embed_uses_nick_in_title_if_available(self): self.assertEqual(embed.title, "Cat lover (Mr. Hemlock)") - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_ignores_everyone_role(self): """Created `!user` embeds should not contain mention of the @everyone-role.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -386,8 +395,8 @@ def test_create_user_embed_ignores_everyone_role(self): embed = asyncio.run(self.cog.create_user_embed(ctx, user)) - self.assertIn("&Admins", embed.description) - self.assertNotIn("&Everyone", embed.description) + self.assertIn("&Admins", embed.fields[1].value) + self.assertNotIn("&Everyone", embed.fields[1].value) @unittest.mock.patch(f"{COG_PATH}.expanded_user_infraction_counts", new_callable=unittest.mock.AsyncMock) @unittest.mock.patch(f"{COG_PATH}.user_nomination_counts", new_callable=unittest.mock.AsyncMock) @@ -398,8 +407,8 @@ def test_create_user_embed_expanded_information_in_moderation_channels(self, nom moderators_role = helpers.MockRole(name='Moderators') moderators_role.colour = 100 - infraction_counts.return_value = "expanded infractions info" - nomination_counts.return_value = "nomination info" + infraction_counts.return_value = ("Infractions", "expanded infractions info") + nomination_counts.return_value = ("Nominations", "nomination info") user = helpers.MockMember(id=314, roles=[moderators_role], top_role=moderators_role) embed = asyncio.run(self.cog.create_user_embed(ctx, user)) @@ -409,20 +418,19 @@ def test_create_user_embed_expanded_information_in_moderation_channels(self, nom self.assertEqual( textwrap.dedent(f""" - **User Information** Created: {"1 year ago"} Profile: {user.mention} ID: {user.id} + """).strip(), + embed.fields[0].value + ) - **Member Information** + self.assertEqual( + textwrap.dedent(f""" Joined: {"1 year ago"} Roles: &Moderators - - expanded infractions info - - nomination info """).strip(), - embed.description + embed.fields[1].value ) @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new_callable=unittest.mock.AsyncMock) @@ -433,7 +441,7 @@ def test_create_user_embed_basic_information_outside_of_moderation_channels(self moderators_role = helpers.MockRole(name='Moderators') moderators_role.colour = 100 - infraction_counts.return_value = "basic infractions info" + infraction_counts.return_value = ("Infractions", "basic infractions info") user = helpers.MockMember(id=314, roles=[moderators_role], top_role=moderators_role) embed = asyncio.run(self.cog.create_user_embed(ctx, user)) @@ -442,21 +450,30 @@ def test_create_user_embed_basic_information_outside_of_moderation_channels(self self.assertEqual( textwrap.dedent(f""" - **User Information** Created: {"1 year ago"} Profile: {user.mention} ID: {user.id} + """).strip(), + embed.fields[0].value + ) - **Member Information** + self.assertEqual( + textwrap.dedent(f""" Joined: {"1 year ago"} Roles: &Moderators - - basic infractions info """).strip(), - embed.description + embed.fields[1].value + ) + + self.assertEqual( + "basic infractions info", + embed.fields[3].value ) - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_uses_top_role_colour_when_user_has_roles(self): """The embed should be created with the colour of the top role, if a top role is available.""" ctx = helpers.MockContext() @@ -469,7 +486,10 @@ def test_create_user_embed_uses_top_role_colour_when_user_has_roles(self): self.assertEqual(embed.colour, discord.Colour(moderators_role.colour)) - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_uses_blurple_colour_when_user_has_no_roles(self): """The embed should be created with a blurple colour if the user has no assigned roles.""" ctx = helpers.MockContext() @@ -479,7 +499,10 @@ def test_create_user_embed_uses_blurple_colour_when_user_has_no_roles(self): self.assertEqual(embed.colour, discord.Colour.blurple()) - @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value="")) + @unittest.mock.patch( + f"{COG_PATH}.basic_user_infraction_counts", + new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) + ) def test_create_user_embed_uses_png_format_of_user_avatar_as_thumbnail(self): """The embed thumbnail should be set to the user's avatar in `png` format.""" ctx = helpers.MockContext() From fd403522896eeb5ffdf10eb5fa1dd0616df32486 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Fri, 14 Aug 2020 21:52:57 +0100 Subject: [PATCH 5/8] Add status information to user command --- bot/cogs/information.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 34a85a86b3..8c5806898f 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -6,7 +6,7 @@ from string import Template from typing import Any, Mapping, Optional, Tuple, Union -from discord import ChannelType, Colour, Embed, Guild, Member, Message, Role, Status, utils +from discord import ChannelType, Colour, CustomActivity, Embed, Guild, Member, Message, Role, Status, utils from discord.abc import GuildChannel from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group from discord.utils import escape_markdown @@ -223,13 +223,18 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: # Custom status custom_status = '' for activity in user.activities: - # Check activity.state for None value if user has a custom status set - # This guards against a custom status with an emoji but no text, which will cause - # escape_markdown to raise an exception - # This can be reworked after a move to d.py 1.3.0+, which adds a CustomActivity class - if activity.name == 'Custom Status' and activity.state: - state = escape_markdown(activity.state) - custom_status = f'Status: {state}\n' + if isinstance(activity, CustomActivity): + state = "" + + if activity.name: + state = escape_markdown(activity.name) + + emoji = "" + if activity.emoji: + if not activity.emoji.id: + emoji += activity.emoji.name + " " + + custom_status = f'Status: {emoji}{state}\n' name = str(user) if user.nick: From e8bd4f6d2316f351ad2c11b0b4db160939ab6ed5 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 16 Aug 2020 20:59:23 +0100 Subject: [PATCH 6/8] Re-align status icons --- bot/cogs/information.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 8c5806898f..776a0d4744 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -273,9 +273,9 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: ( "Status", textwrap.dedent(f""" - Desktop: {desktop_status} - Web: {web_status} - Mobile: {mobile_status} + {desktop_status} Desktop + {web_status} Web + {mobile_status} Mobile """).strip() ) ] From 075110f6300da0525dec0aadb6530409549a02f5 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 23 Aug 2020 14:36:06 +0100 Subject: [PATCH 7/8] Address review comments from @kwzrd --- bot/cogs/information.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index 776a0d4744..c9412948ac 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -20,6 +20,12 @@ log = logging.getLogger(__name__) +STATUS_EMOTES = { + Status.offline: constants.Emojis.status_offline, + Status.dnd: constants.Emojis.status_dnd, + Status.idle: constants.Emojis.status_idle +} + class Information(Cog): """A cog with commands for generating embeds with server info, such as server stats and user info.""" @@ -184,18 +190,6 @@ async def server_info(self, ctx: Context) -> None: await ctx.send(embed=embed) - @staticmethod - def status_to_emoji(status: Status) -> str: - """Convert a Discord status into the relevant emoji.""" - if status is Status.offline: - return constants.Emojis.status_offline - elif status is Status.dnd: - return constants.Emojis.status_dnd - elif status is Status.idle: - return constants.Emojis.status_idle - else: - return constants.Emojis.status_online - @command(name="user", aliases=["user_info", "member", "member_info"]) async def user_info(self, ctx: Context, user: Member = None) -> None: """Returns info about a user.""" @@ -231,6 +225,7 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: emoji = "" if activity.emoji: + # Confirm that the emoji is not a custom emoji since we cannot use them. if not activity.emoji.id: emoji += activity.emoji.name + " " @@ -240,18 +235,18 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: if user.nick: name = f"{user.nick} ({name})" - badges = "" + badges = [] for badge, is_set in user.public_flags: - if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}")): - badges += emoji + " " + if is_set and (emoji := getattr(constants.Emojis, f"badge_{badge}", None)): + badges.append(emoji) joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - desktop_status = self.status_to_emoji(user.desktop_status) - web_status = self.status_to_emoji(user.web_status) - mobile_status = self.status_to_emoji(user.mobile_status) + desktop_status = STATUS_EMOTES.get(user.desktop_status, constants.Emojis.status_online) + web_status = STATUS_EMOTES.get(user.web_status, constants.Emojis.status_online) + mobile_status = STATUS_EMOTES.get(user.mobile_status, constants.Emojis.status_online) fields = [ ( @@ -290,7 +285,7 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: # Let's build the embed now embed = Embed( title=name, - description=badges + description=" ".join(badges) ) for field_name, field_content in fields: From 2448c0530e24cb0aacb733f17dea7a6830fdd98b Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 23 Aug 2020 15:11:41 +0100 Subject: [PATCH 8/8] Don't just exclude custom emoji, include the name of the emote --- bot/cogs/information.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/information.py b/bot/cogs/information.py index c9412948ac..3ec6c33af1 100644 --- a/bot/cogs/information.py +++ b/bot/cogs/information.py @@ -225,9 +225,11 @@ async def create_user_embed(self, ctx: Context, user: Member) -> Embed: emoji = "" if activity.emoji: - # Confirm that the emoji is not a custom emoji since we cannot use them. + # If an emoji is unicode use the emoji, else write the emote like :abc: if not activity.emoji.id: emoji += activity.emoji.name + " " + else: + emoji += f"`:{activity.emoji.name}:` " custom_status = f'Status: {emoji}{state}\n'