Ajout de la monétisation V0.1
This commit is contained in:
parent
cd0fc4c488
commit
879355f792
29 changed files with 698 additions and 72 deletions
205
utils/premium.py
Normal file
205
utils/premium.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue