diff --git a/BOT/botscript.py b/BOT/botscript.py index 8cb878e..dedb7c5 100644 --- a/BOT/botscript.py +++ b/BOT/botscript.py @@ -93,6 +93,7 @@ from .utils import ( invalidate_entitled_guilds_cache, is_admin, is_blacklisted, + gate_entitle, is_guild_entitled, get_guild_tier, permission_fail, @@ -640,8 +641,9 @@ async def comp_perm_error(interaction, error): -@is_blacklisted() -@is_admin() +@is_blacklisted() +@is_admin() +@gate_entitle("standard") @bot.tree.command( name="quick-log", description=command_locale("Quickly set an alarm for this squadron in this channel", "commands.quick_log.description") @@ -1979,6 +1981,7 @@ RECAP_THEME_CHOICES = [ @is_blacklisted() +@gate_entitle("standard") @bot.tree.command(name="sq-card", description=command_locale("Generate a season recap card for a squadron", "commands.sq_card.description")) @app_commands.describe( season=command_locale("The season to generate the card for", "commands.common.season"), @@ -3414,6 +3417,7 @@ class CardPlayerSelectView(View): @is_blacklisted() +@gate_entitle("standard") @bot.tree.command(name="card", description=command_locale("Generate a season recap card for a player", "commands.card.description")) @app_commands.describe( season=command_locale("The season to generate the card for", "commands.common.season"), @@ -6232,6 +6236,7 @@ class MetaManagementView(discord.ui.View): @is_blacklisted() @is_admin() +@gate_entitle("standard") @bot.tree.command(name='meta-management', description=command_locale('Manage meta data access settings for this server', "commands.meta_management.description")) async def meta_management(interaction: discord.Interaction): """Manage meta data access settings for the guild's squadron. @@ -6564,6 +6569,7 @@ class MetaResultsView(discord.ui.View): @is_blacklisted() +@gate_entitle("standard") @bot.tree.command(name='meta', description=command_locale('Search squadron meta roster by vehicle name', "commands.meta.description")) @discord.app_commands.describe(vehicle=command_locale("Vehicle name to search for", "commands.meta.vehicle")) @discord.app_commands.autocomplete(vehicle=meta_vehicle_autocomplete) @@ -7011,6 +7017,7 @@ async def translate_message( @is_blacklisted() +@gate_entitle("standard") @bot.tree.command(name="sq-track", description=command_locale("Track a squadron and compare stats against the last check", "commands.sq_track.description")) @app_commands.describe( squadron_short_name=command_locale("Short name of the squadron to track", "commands.sq_track.squadron_short_name") @@ -7296,6 +7303,7 @@ class ConsistencyPaginatorView(discord.ui.View): @is_blacklisted() +@gate_entitle("standard") @bot.tree.command(name="analytics", description=command_locale("View advanced SQB analytics for a squadron", "commands.analytics.description")) @app_commands.describe( squadron=command_locale("Squadron short name", "commands.common.squadron_short"), diff --git a/BOT/locales/cs.json b/BOT/locales/cs.json index f6649c1..8e98e6f 100644 --- a/BOT/locales/cs.json +++ b/BOT/locales/cs.json @@ -839,7 +839,11 @@ "reason_line": "**Důvod:** {reason}", "access_denied_title": "⛔ Přístup odepřen", "no_permission_desc": "Nemáš oprávnění použít tento příkaz.", - "unexpected_error_title": "❗ Chyba, nahlas ji...." + "unexpected_error_title": "❗ Chyba, nahlas ji....", + "tier_gate_title": "🔒 Vyžadováno prémium", + "tier_gate_standard_desc": "Tento příkaz vyžaduje úroveň **Standard** nebo vyšší. Použij `/unlock` k odběru.", + "tier_gate_pro_desc": "Tento příkaz vyžaduje úroveň **Pro** nebo vyšší. Použij `/unlock` k odběru.", + "tier_gate_max_desc": "Tento příkaz vyžaduje úroveň **Max**. Použij `/unlock` k odběru." }, "weekly_br": { "title_wildcard": "Týdenní zpráva BR — {br} BR", diff --git a/BOT/locales/de.json b/BOT/locales/de.json index 7c732ab..7260cee 100644 --- a/BOT/locales/de.json +++ b/BOT/locales/de.json @@ -839,7 +839,11 @@ "reason_line": "**Grund:** {reason}", "access_denied_title": "⛔ Zugriff verweigert", "no_permission_desc": "Du hast keine Berechtigung für diesen Command.", - "unexpected_error_title": "❗ Fehler, bitte melden...." + "unexpected_error_title": "❗ Fehler, bitte melden....", + "tier_gate_title": "🔒 Premium erforderlich", + "tier_gate_standard_desc": "Dieser Befehl benötigt eine **Standard**-Berechtigung oder höher. Nutze `/unlock`, um zu abonnieren.", + "tier_gate_pro_desc": "Dieser Befehl benötigt eine **Pro**-Berechtigung oder höher. Nutze `/unlock`, um zu abonnieren.", + "tier_gate_max_desc": "Dieser Befehl benötigt eine **Max**-Berechtigung. Nutze `/unlock`, um zu abonnieren." }, "weekly_br": { "title_wildcard": "Wöchentlicher BR-Bericht — {br} BR", diff --git a/BOT/locales/en.json b/BOT/locales/en.json index 54b91c5..114bdb3 100644 --- a/BOT/locales/en.json +++ b/BOT/locales/en.json @@ -840,7 +840,11 @@ "reason_line": "**Reason:** {reason}", "access_denied_title": "⛔ Access Denied", "no_permission_desc": "You do not have permission to use this command.", - "unexpected_error_title": "❗ Error, report this...." + "unexpected_error_title": "❗ Error, report this....", + "tier_gate_title": "🔒 Premium Required", + "tier_gate_standard_desc": "This command requires a **Standard** entitlement or higher. Use `/unlock` to subscribe.", + "tier_gate_pro_desc": "This command requires a **Pro** entitlement or higher. Use `/unlock` to subscribe.", + "tier_gate_max_desc": "This command requires a **Max** entitlement. Use `/unlock` to subscribe." }, "weekly_br": { "title_wildcard": "Weekly BR Report — {br} BR", diff --git a/BOT/locales/es.json b/BOT/locales/es.json index 7915148..1f0d6fc 100644 --- a/BOT/locales/es.json +++ b/BOT/locales/es.json @@ -839,7 +839,11 @@ "reason_line": "**Motivo:** {reason}", "access_denied_title": "⛔ Acceso denegado", "no_permission_desc": "No tienes permiso para usar este comando.", - "unexpected_error_title": "❗ Error, repórtalo...." + "unexpected_error_title": "❗ Error, repórtalo....", + "tier_gate_title": "🔒 Premium requerido", + "tier_gate_standard_desc": "Este comando requiere una suscripción **Standard** o superior. Usa `/unlock` para suscribirte.", + "tier_gate_pro_desc": "Este comando requiere una suscripción **Pro** o superior. Usa `/unlock` para suscribirte.", + "tier_gate_max_desc": "Este comando requiere una suscripción **Max**. Usa `/unlock` para suscribirte." }, "weekly_br": { "title_wildcard": "Informe BR Semanal — {br} BR", diff --git a/BOT/locales/fr.json b/BOT/locales/fr.json index f2a8b04..f63e984 100644 --- a/BOT/locales/fr.json +++ b/BOT/locales/fr.json @@ -839,7 +839,11 @@ "reason_line": "**Raison :** {reason}", "access_denied_title": "⛔ Accès refusé", "no_permission_desc": "Tu n'as pas la permission d'utiliser cette commande.", - "unexpected_error_title": "❗ Erreur, signale-la...." + "unexpected_error_title": "❗ Erreur, signale-la....", + "tier_gate_title": "🔒 Premium requis", + "tier_gate_standard_desc": "Cette commande nécessite un abonnement **Standard** ou supérieur. Utilise `/unlock` pour souscrire.", + "tier_gate_pro_desc": "Cette commande nécessite un abonnement **Pro** ou supérieur. Utilise `/unlock` pour souscrire.", + "tier_gate_max_desc": "Cette commande nécessite un abonnement **Max**. Utilise `/unlock` pour souscrire." }, "weekly_br": { "title_wildcard": "Rapport BR hebdomadaire — {br} BR", diff --git a/BOT/locales/it.json b/BOT/locales/it.json index 690f95e..9bea177 100644 --- a/BOT/locales/it.json +++ b/BOT/locales/it.json @@ -839,7 +839,11 @@ "reason_line": "**Motivo:** {reason}", "access_denied_title": "⛔ Accesso negato", "no_permission_desc": "Non hai il permesso di usare questo comando.", - "unexpected_error_title": "❗ Errore, segnalalo...." + "unexpected_error_title": "❗ Errore, segnalalo....", + "tier_gate_title": "🔒 Premium richiesto", + "tier_gate_standard_desc": "Questo comando richiede un'iscrizione **Standard** o superiore. Usa `/unlock` per abbonarti.", + "tier_gate_pro_desc": "Questo comando richiede un'iscrizione **Pro** o superiore. Usa `/unlock` per abbonarti.", + "tier_gate_max_desc": "Questo comando richiede un'iscrizione **Max**. Usa `/unlock` per abbonarti." }, "weekly_br": { "title_wildcard": "Report BR Settimanale — {br} BR", diff --git a/BOT/locales/pl.json b/BOT/locales/pl.json index 15f3674..39a948f 100644 --- a/BOT/locales/pl.json +++ b/BOT/locales/pl.json @@ -839,7 +839,11 @@ "reason_line": "**Powód:** {reason}", "access_denied_title": "⛔ Odmowa dostępu", "no_permission_desc": "Nie masz uprawnień do użycia tej komendy.", - "unexpected_error_title": "❗ Błąd, zgłoś to...." + "unexpected_error_title": "❗ Błąd, zgłoś to....", + "tier_gate_title": "🔒 Wymagane Premium", + "tier_gate_standard_desc": "Ta komenda wymaga subskrypcji **Standard** lub wyższej. Użyj `/unlock`, aby się zapisać.", + "tier_gate_pro_desc": "Ta komenda wymaga subskrypcji **Pro** lub wyższej. Użyj `/unlock`, aby się zapisać.", + "tier_gate_max_desc": "Ta komenda wymaga subskrypcji **Max**. Użyj `/unlock`, aby się zapisać." }, "weekly_br": { "title_wildcard": "Tygodniowy raport BR — {br} BR", diff --git a/BOT/locales/pt.json b/BOT/locales/pt.json index c32c984..c880c55 100644 --- a/BOT/locales/pt.json +++ b/BOT/locales/pt.json @@ -839,7 +839,11 @@ "reason_line": "**Motivo:** {reason}", "access_denied_title": "⛔ Acesso negado", "no_permission_desc": "Você não tem permissão para usar este comando.", - "unexpected_error_title": "❗ Erro, reporte isso...." + "unexpected_error_title": "❗ Erro, reporte isso....", + "tier_gate_title": "🔒 Premium necessário", + "tier_gate_standard_desc": "Este comando requer assinatura **Standard** ou superior. Use `/unlock` para assinar.", + "tier_gate_pro_desc": "Este comando requer assinatura **Pro** ou superior. Use `/unlock` para assinar.", + "tier_gate_max_desc": "Este comando requer assinatura **Max**. Use `/unlock` para assinar." }, "weekly_br": { "title_wildcard": "Relatório BR Semanal — {br} BR", diff --git a/BOT/locales/ru.json b/BOT/locales/ru.json index 6e76159..9c713d3 100644 --- a/BOT/locales/ru.json +++ b/BOT/locales/ru.json @@ -839,7 +839,11 @@ "reason_line": "**Причина:** {reason}", "access_denied_title": "⛔ Доступ запрещён", "no_permission_desc": "У вас нет прав для использования этой команды.", - "unexpected_error_title": "❗ Ошибка, сообщите о ней...." + "unexpected_error_title": "❗ Ошибка, сообщите о ней....", + "tier_gate_title": "🔒 Требуется Premium", + "tier_gate_standard_desc": "Эта команда требует подписку **Standard** или выше. Используйте `/unlock`, чтобы оформить.", + "tier_gate_pro_desc": "Эта команда требует подписку **Pro** или выше. Используйте `/unlock`, чтобы оформить.", + "tier_gate_max_desc": "Эта команда требует подписку **Max**. Используйте `/unlock`, чтобы оформить." }, "weekly_br": { "title_wildcard": "Еженедельный отчёт BR — {br} BR", diff --git a/BOT/locales/uk.json b/BOT/locales/uk.json index b42d8a1..4def0c5 100644 --- a/BOT/locales/uk.json +++ b/BOT/locales/uk.json @@ -839,7 +839,11 @@ "reason_line": "**Причина:** {reason}", "access_denied_title": "⛔ Доступ заборонено", "no_permission_desc": "У вас немає прав для використання цієї команди.", - "unexpected_error_title": "❗ Помилка, повідомте про неї...." + "unexpected_error_title": "❗ Помилка, повідомте про неї....", + "tier_gate_title": "🔒 Потрібен Premium", + "tier_gate_standard_desc": "Ця команда потребує підписки **Standard** або вище. Використайте `/unlock`, щоб оформити.", + "tier_gate_pro_desc": "Ця команда потребує підписки **Pro** або вище. Використайте `/unlock`, щоб оформити.", + "tier_gate_max_desc": "Ця команда потребує підписки **Max**. Використайте `/unlock`, щоб оформити." }, "weekly_br": { "title_wildcard": "Тижневий звіт BR — {br} BR", diff --git a/BOT/locales/zh-CN.json b/BOT/locales/zh-CN.json index 2fac518..ccf76cf 100644 --- a/BOT/locales/zh-CN.json +++ b/BOT/locales/zh-CN.json @@ -841,7 +841,11 @@ "reason_line": "**原因:** {reason}", "access_denied_title": "⛔ 访问被拒绝", "no_permission_desc": "你没有权限使用此命令。", - "unexpected_error_title": "❗ 出错了,请上报...." + "unexpected_error_title": "❗ 出错了,请上报....", + "tier_gate_title": "🔒 需要订阅", + "tier_gate_standard_desc": "此命令需要 **Standard** 或更高等级的订阅。使用 `/unlock` 订阅。", + "tier_gate_pro_desc": "此命令需要 **Pro** 或更高等级的订阅。使用 `/unlock` 订阅。", + "tier_gate_max_desc": "此命令需要 **Max** 订阅。使用 `/unlock` 订阅。" }, "weekly_br": { "title_wildcard": "周BR报告 — {br} BR", diff --git a/BOT/stack_manager.py b/BOT/stack_manager.py index 6cccaac..287c7d1 100644 --- a/BOT/stack_manager.py +++ b/BOT/stack_manager.py @@ -22,6 +22,7 @@ from .utils import ( load_json, write_json, is_blacklisted, + gate_entitle, permission_fail, esc, collect_command_stats, @@ -1163,6 +1164,7 @@ def register_commands(bot: commands.Bot) -> None: """ @is_blacklisted() + @gate_entitle("standard") @bot.tree.command(name="stack-create", description=command_locale("Create a new player stack", "commands.stack_create.description")) @app_commands.describe(vehicle=command_locale("What vehicle will you start with?", "commands.stack_create.vehicle")) async def stack_create(interaction: discord.Interaction, vehicle: str) -> None: @@ -1214,6 +1216,7 @@ def register_commands(bot: commands.Bot) -> None: await permission_fail(interaction, error) @is_blacklisted() + @gate_entitle("standard") @bot.tree.command(name="stack-manage", description=command_locale("Re-post your active stack embed to this channel", "commands.stack_manage.description")) async def stack_manage(interaction: discord.Interaction) -> None: await collect_command_stats(interaction) diff --git a/BOT/task_executors.py b/BOT/task_executors.py index a61f47d..05f0547 100644 --- a/BOT/task_executors.py +++ b/BOT/task_executors.py @@ -818,7 +818,7 @@ async def _process_squadron_points( # SAFE CHUNK BUILDER max_len = 1024 chunks = [] - buf = "```\nName Chg Now KDR KPS\n" + buf = "```\nName Change Now KDR KPS\n" for uid, (delta, now) in sorted_changes: name_raw = ( diff --git a/BOT/utils.py b/BOT/utils.py index 4547b2e..0dbe0ae 100644 --- a/BOT/utils.py +++ b/BOT/utils.py @@ -490,6 +490,19 @@ class BlacklistCheckFailure(app_commands.CheckFailure): pass +class TierGateFailure(app_commands.CheckFailure): + """Raised when a guild's tier is below what a command requires. + + The minimum required tier is carried on ``required_tier`` so + ``permission_fail`` can render the matching localized embed. + """ + + def __init__(self, required_tier: str, current_tier: Optional[str] = None): + super().__init__(f"This command requires the '{required_tier}' tier or higher.") + self.required_tier = required_tier + self.current_tier = current_tier + + # ============================================================================ # PERMISSION DECORATORS # ============================================================================ @@ -551,6 +564,27 @@ def is_blacklisted(): return app_commands.check(predicate) +def gate_entitle(required_tier: str): + """Return an app-command check that requires a minimum entitlement tier. + + Accepts 'standard', 'pro', or 'max'. Guilds with no active entitlement, or + with a tier strictly lower than ``required_tier``, are rejected with a + ``TierGateFailure`` carrying the required tier so the error handler can + render the matching localized embed. + """ + if required_tier not in TIER_ORDER: + raise ValueError(f"Unknown tier {required_tier!r}; expected one of {TIER_ORDER}") + + async def predicate(interaction: discord.Interaction): + if interaction.guild_id is None: + raise TierGateFailure(required_tier, None) + current = await get_guild_tier(interaction.guild_id) + if _tier_rank(current) < _tier_rank(required_tier): + raise TierGateFailure(required_tier, current) + return True + return app_commands.check(predicate) + + # ============================================================================ # PERMISSION ERROR HANDLER # ============================================================================ @@ -574,6 +608,13 @@ async def permission_fail(interaction: discord.Interaction, error): description=f"{error.args[0]}", color=discord.Color.orange() ) + elif isinstance(error, TierGateFailure): + desc_key = f"permission.tier_gate_{error.required_tier}_desc" + embed = discord.Embed( + title=t(lang, "permission.tier_gate_title"), + description=t(lang, desc_key), + color=discord.Color.gold() + ) elif isinstance(error, app_commands.CheckFailure): embed = discord.Embed( title=t(lang, "permission.access_denied_title"), diff --git a/web/views/premium.ejs b/web/views/premium.ejs index 9cb97d4..2ea7f5d 100644 --- a/web/views/premium.ejs +++ b/web/views/premium.ejs @@ -370,11 +370,11 @@