Kuby/utils/premium.py

206 lines
8.6 KiB
Python
Raw Normal View History

2026-07-06 16:08:14 +02:00
import os
import json
import logging
import asyncio
import disnake
from disnake.ext import commands
from typing import Tuple, Optional, List
from dotenv import load_dotenv
# Logger instance
kuby_logger = logging.getLogger("KubyBot")
# Project paths
UTILS_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(UTILS_DIR)
# Explicitly load .env file from the project root
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
# Whitelist file location
WHITELIST_PARTNERS_FILE = os.path.join(PROJECT_ROOT, "whitelist_partners.json")
async def load_partners_whitelist() -> List[int]:
"""Asynchronously loads the whitelist partners JSON file.
Returns:
List[int]: A list of whitelisted guild IDs.
"""
def read_file():
if not os.path.exists(WHITELIST_PARTNERS_FILE):
# Create a default empty file if it doesn't exist
try:
with open(WHITELIST_PARTNERS_FILE, "w", encoding="utf-8") as f:
json.dump({"partners": []}, f, indent=4)
return []
except Exception as e:
kuby_logger.error(f"Failed to create default whitelist file: {e}")
return []
try:
with open(WHITELIST_PARTNERS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("partners", [])
except (json.JSONDecodeError, IOError) as e:
kuby_logger.error(f"Failed to read whitelist file at {WHITELIST_PARTNERS_FILE}: {e}")
return []
return await asyncio.to_thread(read_file)
def get_sku_ids() -> Tuple[Optional[int], Optional[int], Optional[int]]:
"""Loads and returns the SKU IDs from the environment variables.
Returns:
Tuple[Optional[int], Optional[int], Optional[int]]: Starter, Medium, and Large SKU IDs.
"""
def parse_env_int(key: str) -> Optional[int]:
val = os.getenv(key)
if not val:
return None
try:
return int(val.strip())
except ValueError:
kuby_logger.error(f"Invalid environment variable format for {key}: '{val}' is not an integer.")
return None
starter = parse_env_int("STARTER_SKU_ID")
medium = parse_env_int("MEDIUM_SKU_ID")
# Supports both LARGE_SKU_ID and MAX_SKU_ID
large = parse_env_int("LARGE_SKU_ID") or parse_env_int("MAX_SKU_ID")
return starter, medium, large
def check_premium_tier():
"""A custom disnake command check that enforces premium tier limitations based on server size
and Discord Entitlements.
IMPORTANT:
- Set DISABLE_PREMIUM_SKU_CHECK=true to bypass SKU entitlement checks (useful for dev/test).
"""
async def predicate(interaction: disnake.ApplicationCommandInteraction) -> bool:
# Dev/test bypass: allow commands even when SKU env vars aren't configured.
if os.getenv("DISABLE_PREMIUM_SKU_CHECK", "").lower() in {"1", "true", "yes", "y", "on"}:
kuby_logger.warning(
"[PREMIUM CHECK] Bypass enabled via DISABLE_PREMIUM_SKU_CHECK. "
f"guild_id={getattr(interaction.guild, 'id', None)}"
)
return True
# Debugging Log
debug_guild_id = interaction.guild.id if interaction.guild else None
debug_member_count = interaction.guild.member_count if interaction.guild else None
try:
debug_entitlements = [{"sku_id": ent.sku_id, "active": ent.is_active()} for ent in interaction.entitlements]
except Exception as e:
debug_entitlements = f"Error: {e}"
# Resolve SKU IDs from env for easier diagnosis
starter_id, medium_id, large_id = get_sku_ids()
kuby_logger.debug(
"[DEBUG PREMIUM CHECK] "
f"Guild ID: {debug_guild_id} | "
f"Member Count: {debug_member_count} | "
f"Entitlements: {debug_entitlements} | "
f"Env SKUs: starter={starter_id} medium={medium_id} large={large_id}"
)
# 1. Refuse access if the command was run in DMs
if interaction.guild is None:
kuby_logger.warning("Access Denied: Command run in DMs.")
await interaction.response.send_message(
"❌ Cette commande ne peut être exécutée que sur un serveur Discord.",
ephemeral=True
)
return False
guild_id = interaction.guild.id
# 2. Check the Partners Whitelist
try:
partners = await load_partners_whitelist()
if guild_id in partners:
kuby_logger.info(f"Access Granted: Guild {guild_id} ({interaction.guild.name}) is in whitelist.")
return True
except Exception as e:
# Safe fail-closed pattern: Log error and continue to premium checks if whitelist loading crashed.
kuby_logger.error(f"Error checking partners whitelist for guild {guild_id}: {e}", exc_info=True)
# 3. Retrieve and validate the guild member count
member_count = interaction.guild.member_count
if member_count is None or not isinstance(member_count, int) or member_count <= 0:
kuby_logger.error(f"Fail-Safe triggered: Invalid member count {member_count} for guild {guild_id}.")
await interaction.response.send_message(
"❌ Impossible de vérifier la taille du serveur par sécurité. Veuillez réessayer plus tard.",
ephemeral=True
)
return False
# 4. Check active entitlements and SKUs
try:
active_skus = {ent.sku_id for ent in interaction.entitlements if ent.is_active()}
except Exception as e:
kuby_logger.error(f"Fail-Closed: Error retrieving entitlements for guild {guild_id}: {e}", exc_info=True)
await interaction.response.send_message(
"❌ Erreur lors de la vérification de votre abonnement auprès de Discord. Veuillez réessayer plus tard.",
ephemeral=True
)
return False
# Identify required tier & allowed SKUs
if member_count < 500:
required_tier_name = "Starter"
allowed_skus = [starter_id, medium_id, large_id]
elif 500 <= member_count <= 2500:
required_tier_name = "Medium"
allowed_skus = [medium_id, large_id]
else:
required_tier_name = "Large"
allowed_skus = [large_id]
# Filter out unconfigured SKUs (None values)
allowed_skus = [sku for sku in allowed_skus if sku is not None]
# If no valid SKU ID is configured in the environment for this tier, block access
if not allowed_skus:
kuby_logger.error(
f"Configuration Error: No SKU IDs configured in .env for tier '{required_tier_name}' "
f"(Starter: {starter_id}, Medium: {medium_id}, Large: {large_id}). Access blocked."
)
await interaction.response.send_message(
"❌ Une erreur de configuration empêche l'accès à cette commande premium. "
"Veuillez contacter le support de l'association Omega Kube.",
ephemeral=True
)
return False
# Verify entitlement matching one of the allowed SKU IDs
has_valid_sku = any(sku in active_skus for sku in allowed_skus)
if has_valid_sku:
kuby_logger.info(
f"Access Granted: Guild {guild_id} ({interaction.guild.name}) "
f"passed premium check (Members: {member_count}, Tier: {required_tier_name})."
)
return True
# 5. Block access and send polite ephemeral instruction message
kuby_logger.info(
f"Access Denied: Guild {guild_id} ({interaction.guild.name}) "
f"has {member_count} members but no valid SKU. Active SKUs: {active_skus}."
)
await interaction.response.send_message(
f"✨ **Fonctionnalité Premium** ✨\n\n"
f"Merci d'utiliser le bot **Kuby** ! Cette commande nécessite un abonnement actif.\n"
f"📊 **Statistiques de votre serveur :**\n"
f"- Nombre de membres : `{member_count}`\n"
f"- Palier requis : **Palier {required_tier_name}**\n\n"
f"💡 **Comment s'abonner ?**\n"
f"Vous pouvez vous abonner directement depuis le profil du bot Discord ou via l'App Directory.\n\n"
f"Si vous êtes un partenaire officiel de l'association **Omega Kube**, "
f"veuillez contacter un administrateur pour être ajouté à la Whitelist.",
ephemeral=True
)
return False
return commands.check(predicate)