Skip to content

Commit a7c7744

Browse files
committed
✨ Add help extension
1 parent fcfe5f5 commit a7c7744

File tree

6 files changed

+352
-19
lines changed

6 files changed

+352
-19
lines changed

src/custom/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
BridgeExtContext,
1414
)
1515

16-
from src.i18n.classes import ExtensionTranslation, TranslationWrapper, apply_locale
16+
from src.i18n.classes import ExtensionTranslation, RawTranslation, TranslationWrapper, apply_locale
1717

1818
if TYPE_CHECKING:
1919
from src.database.models import Guild, User
@@ -23,7 +23,9 @@
2323

2424
class ApplicationContext(bridge.BridgeApplicationContext):
2525
def __init__(self, bot: "Bot", interaction: discord.Interaction) -> None:
26-
self.translations: TranslationWrapper = TranslationWrapper({}, "en-US") # empty placeholder
26+
self.translations: TranslationWrapper[dict[str, RawTranslation]] = TranslationWrapper(
27+
{}, "en-US"
28+
) # empty placeholder
2729
super().__init__(bot=bot, interaction=interaction)
2830
self.bot: Bot
2931
self.user_obj: User | None = None

src/extensions/help/__init__.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (c) NiceBots all rights reserved
2+
from collections import defaultdict
3+
from functools import cached_property
4+
from typing import Any, Final, final, override
5+
6+
import discord
7+
from discord.ext import commands
8+
from discord.ext import pages as paginator
9+
10+
from src import custom
11+
from src.extensions.help.pages.classes import (
12+
HelpCategoryTranslation,
13+
)
14+
from src.i18n.classes import RawTranslation, TranslationWrapper, apply_locale
15+
16+
from .pages import help_translation
17+
18+
19+
def get_gradient_color(shade_index: int, color_index: int, max_shade: int = 50, max_color: int = 10) -> int:
20+
"""Generate a color from a two-dimensional gradient system using bright pastel colors.
21+
22+
Args:
23+
----
24+
shade_index (int): Index for shade selection (0 to max_shade)
25+
color_index (int): Index for base color selection (0 to max_color)
26+
max_shade (int): Maximum value for shade_index (default 50)
27+
max_color (int): Maximum value for color_index (default 10)
28+
29+
Returns:
30+
-------
31+
int: Color as a 24-bit integer
32+
33+
"""
34+
# Normalize indices to 0-1 range
35+
shade_factor = max(0, min(1, shade_index / max_shade))
36+
color_factor = max(0, min(1, color_index / max_color))
37+
38+
# Bright pastel base colors
39+
base_colors = [
40+
(179, 229, 252), # Bright light blue
41+
(225, 190, 231), # Bright lilac
42+
(255, 209, 220), # Bright pink
43+
(255, 224, 178), # Bright peach
44+
(255, 255, 198), # Bright yellow
45+
(200, 230, 201), # Bright mint
46+
(178, 255, 255), # Bright turquoise
47+
(187, 222, 251), # Bright baby blue
48+
(225, 190, 231), # Bright lavender
49+
(255, 236, 179), # Bright cream
50+
(200, 230, 255), # Bright sky blue
51+
]
52+
53+
# Interpolate between colors based on color_factor
54+
color_index_float = color_factor * (len(base_colors) - 1)
55+
color_index_low = int(color_index_float)
56+
color_index_high = min(color_index_low + 1, len(base_colors) - 1)
57+
color_blend = color_index_float - color_index_low
58+
59+
c1 = base_colors[color_index_low]
60+
c2 = base_colors[color_index_high]
61+
62+
# Interpolate between the two closest base colors
63+
base_color = tuple(int(c1[i] * (1 - color_blend) + c2[i] * color_blend) for i in range(3))
64+
65+
# Modified shading approach for brighter colors
66+
if shade_factor < 0.5:
67+
# Darker shades: interpolate towards a very light gray instead of black
68+
shade_factor_adjusted = shade_factor * 2
69+
# Increase minimum brightness (was 120, now 180)
70+
darker_tone = tuple(max(c * 0.8, 180) for c in base_color)
71+
final_color = tuple(
72+
int(darker_tone[i] + (base_color[i] - darker_tone[i]) * shade_factor_adjusted) for i in range(3)
73+
)
74+
else:
75+
# Lighter shades: interpolate towards white
76+
shade_factor_adjusted = (shade_factor - 0.5) * 2
77+
final_color = tuple(
78+
int(base_color[i] * (1 - shade_factor_adjusted) + 255 * shade_factor_adjusted) for i in range(3)
79+
)
80+
81+
# Convert to 24-bit integer
82+
return (final_color[0] << 16) | (final_color[1] << 8) | final_color[2]
83+
84+
85+
class PageIndicatorButton(paginator.PaginatorButton):
86+
def __init__(self) -> None:
87+
super().__init__(button_type="page_indicator", disabled=True, label="", style=discord.ButtonStyle.gray)
88+
89+
90+
@final
91+
class HelpView(paginator.Paginator):
92+
def __init__(
93+
self,
94+
embeds: dict[str, list[discord.Embed]],
95+
ui_translations: TranslationWrapper[dict[str, RawTranslation]],
96+
bot: custom.Bot,
97+
) -> None:
98+
self.bot = bot
99+
100+
the_pages: list[paginator.PageGroup] = [
101+
paginator.PageGroup(
102+
[paginator.Page(embeds=[embed]) for embed in data[1]],
103+
label=data[0],
104+
default=i == 0,
105+
)
106+
for i, data in enumerate(embeds.items())
107+
]
108+
109+
self.embeds = embeds
110+
self.ui_translations = ui_translations
111+
self.page_indicator = PageIndicatorButton()
112+
super().__init__(
113+
the_pages,
114+
show_menu=True,
115+
menu_placeholder=ui_translations.select_category,
116+
custom_buttons=[
117+
paginator.PaginatorButton("first", emoji="⏮️", style=discord.ButtonStyle.blurple),
118+
paginator.PaginatorButton("prev", emoji="◀️", style=discord.ButtonStyle.red),
119+
self.page_indicator,
120+
paginator.PaginatorButton("next", emoji="▶️", style=discord.ButtonStyle.green),
121+
paginator.PaginatorButton("last", emoji="⏭️", style=discord.ButtonStyle.blurple),
122+
],
123+
use_default_buttons=False,
124+
)
125+
126+
@override
127+
def update_buttons(self) -> dict: # pyright: ignore [reportMissingTypeArgument, reportUnknownParameterType]
128+
r = super().update_buttons() # pyright: ignore [reportUnknownVariableType]
129+
if self.show_indicator:
130+
self.buttons["page_indicator"]["object"].label = self.ui_translations.page_indicator.format(
131+
current=self.current_page + 1, total=self.page_count + 1
132+
)
133+
return r # pyright: ignore [reportUnknownVariableType]
134+
135+
136+
def get_categories_embeds(
137+
ui_translations: TranslationWrapper[dict[str, RawTranslation]],
138+
categories: dict[str, TranslationWrapper[HelpCategoryTranslation]],
139+
bot: custom.Bot,
140+
) -> dict[str, list[discord.Embed]]:
141+
embeds: defaultdict[str, list[discord.Embed]] = defaultdict(list)
142+
for i, category in enumerate(categories):
143+
for j, page in enumerate(category.pages.values()): # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue]
144+
embed = discord.Embed(
145+
title=f"{category.name} - {page.title}", # pyright: ignore [reportAttributeAccessIssue]
146+
description=page.description, # pyright: ignore [reportUnknownArgumentType]
147+
color=discord.Color(get_gradient_color(i, j)),
148+
)
149+
if page.quick_tips:
150+
embed.add_field(name=ui_translations.quick_tips_title, value="- " + "\n- ".join(page.quick_tips)) # pyright: ignore [reportUnknownArgumentType]
151+
if page.examples:
152+
embed.add_field(name=ui_translations.examples_title, value="- " + "\n- ".join(page.examples)) # pyright: ignore [reportUnknownArgumentType]
153+
if page.related_commands:
154+
embed.add_field(
155+
name=ui_translations.related_commands_title,
156+
value="- "
157+
+ "\n- ".join(bot.get_application_command(name).mention for name in page.related_commands), # pyright: ignore [reportUnknownArgumentType, reportUnknownVariableType, reportAttributeAccessIssue, reportOptionalMemberAccess]
158+
)
159+
embeds[category.name].append(embed) # pyright: ignore [reportAttributeAccessIssue]
160+
return dict(embeds)
161+
162+
163+
@final
164+
class Help(commands.Cog):
165+
def __init__(self, bot: custom.Bot, ui_translations: dict[str, RawTranslation], locales: set[str]) -> None:
166+
self.bot = bot
167+
self.ui_translations = ui_translations
168+
self.locales = locales
169+
170+
@cached_property
171+
async def embeds(self) -> dict[str, dict[str, list[discord.Embed]]]:
172+
embeds: defaultdict[str, dict[str, list[discord.Embed]]] = defaultdict(dict)
173+
for locale in self.locales:
174+
t = help_translation.get_for_locale(locale)
175+
ui = apply_locale(self.ui_translations, locale)
176+
embeds[locale] = get_categories_embeds(ui, t.categories, self.bot)
177+
return dict(embeds)
178+
179+
@discord.slash_command(
180+
name="help",
181+
integration_types={discord.IntegrationType.user_install, discord.IntegrationType.guild_install},
182+
contexts={
183+
discord.InteractionContextType.guild,
184+
discord.InteractionContextType.private_channel,
185+
discord.InteractionContextType.bot_dm,
186+
},
187+
)
188+
async def help_slash(self, ctx: custom.ApplicationContext) -> None:
189+
paginator = HelpView(
190+
embeds=self.embeds[ctx.locale],
191+
ui_translations=apply_locale(self.ui_translations, ctx.locale),
192+
bot=self.bot,
193+
)
194+
await paginator.respond(ctx.interaction, ephemeral=True)
195+
196+
197+
def setup(bot: custom.Bot, config: dict[str, Any]) -> None: # pyright: ignore [reportExplicitAny]
198+
bot.add_cog(Help(bot, config["translations"], set(config["locales"])))
199+
200+
201+
default: Final = {"enabled": False}
202+
__all__ = ["default", "setup"]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) NiceBots
2+
# SPDX-License-Identifier: MIT
3+
4+
from itertools import chain
5+
from pathlib import Path
6+
7+
import yaml
8+
9+
from .classes import HelpCategoryTranslation, HelpTranslation
10+
11+
# iterate over .y[a]ml files in the same directory as this file
12+
categories: list[HelpCategoryTranslation] = []
13+
14+
for file in chain(Path(__file__).parent.glob("*.yaml"), Path(__file__).parent.glob("*.yml")):
15+
with open(file, encoding="utf-8") as f:
16+
data = yaml.safe_load(f)
17+
categories.append(HelpCategoryTranslation(**data))
18+
19+
categories.sort(key=lambda item: item.order)
20+
21+
help_translation = HelpTranslation(categories=categories)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) NiceBots
2+
# SPDX-License-Identifier: MIT
3+
4+
from src.i18n.classes import RawTranslation, Translation
5+
6+
7+
class HelpPageTranslation(Translation):
8+
title: RawTranslation
9+
description: RawTranslation
10+
category: RawTranslation
11+
quick_tips: list[RawTranslation] | None = None
12+
examples: list[RawTranslation] | None = None
13+
related_commands: list[str] | None = None
14+
15+
16+
class HelpCategoryTranslation(Translation):
17+
name: RawTranslation
18+
description: RawTranslation
19+
pages: dict[str, HelpPageTranslation]
20+
order: int # For sorting categories in the dropdown
21+
22+
23+
class HelpTranslation(Translation):
24+
categories: list[HelpCategoryTranslation]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (c) NiceBots all rights reserved
2+
strings:
3+
quick_tips_title:
4+
en-US: Tips & Tricks
5+
es-ES: Consejos y Trucos
6+
examples_title:
7+
en-US: Usage Examples
8+
es-ES: Ejemplos de Uso
9+
related_commands_title:
10+
en-US: Available Commands
11+
es-ES: Comandos Disponibles
12+
select_category:
13+
en-US: Select a category
14+
es-ES: Selecciona una categoría
15+
page_indicator:
16+
en-US: Page {current} of {total}
17+
es-ES: Página {current} de {total}
18+
commands:
19+
help:
20+
name:
21+
en-US: help
22+
es-ES: ayuda
23+
description:
24+
en-US: Get help with using the bot
25+
es-ES: Obtén ayuda para usar el bot

0 commit comments

Comments
 (0)