Ajout de la monétisation V0.1

This commit is contained in:
Mathis 2026-07-06 16:08:14 +02:00
parent cd0fc4c488
commit 879355f792
29 changed files with 698 additions and 72 deletions

24
TODO.md
View file

@ -1,13 +1,15 @@
# TODO - Ticket: fermeture restreinte après prise en charge # TODO - Désactivation fonctionnalités premium (SKU) & correction accès /ticket
- [ ] Mettre à jour la permission de fermeture côté joueur vs staff pour les tickets (mode général) : ## Étape 1 — Compréhension
- Joueur : uniquement si `TicketStatus.OPEN` - [x] Identifier le check premium bloquant /ticket (utils/premium.py)
- Staff : autorisé même si `TicketStatus.CLAIMED` - [x] Vérifier où le check est appliqué (commandes/ticket/__init__.py)
- [x] Mettre à jour la permission de fermeture côté confirmation `close_confirm` (routeur `on_interaction`) avec la même règle.
- [x] Appliquer la même contrainte au mode recrutement (boutons `recruitment_close` / accept/refuse) : ## Étape 2 — Mise en conformité
- Joueur ne peut pas fermer si ticket déjà pris en charge (si applicable selon votre logique de "claim"). - [x] Désactivation temporaire via env var : DISABLE_PREMIUM_SKU_CHECK=true
- [x] Tester manuellement : - [ ] Ajouter un switch clair pour ré-activer facilement (déjà géré par la même env var)
- Ticket OPEN => fermeture joueur OK
- Ticket CLAIMED => fermeture joueur refusée, staff OK
- Recrutement : fermeture conformes à la contrainte ## Étape 3 — Validation
- [ ] Lancer un lint/tests rapides ou démarrer le bot (si possible)
- [ ] Vérifier que /ticket répond sur le serveur bloqué (Omega Kube)

View file

@ -9,6 +9,7 @@ try:
except ImportError: except ImportError:
from backports.zoneinfo import ZoneInfo from backports.zoneinfo import ZoneInfo
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from utils.premium import check_premium_tier
PARIS_TZ = ZoneInfo("Europe/Paris") PARIS_TZ = ZoneInfo("Europe/Paris")
DATE_FORMAT = "%d/%m/%Y %H:%M" DATE_FORMAT = "%d/%m/%Y %H:%M"
@ -259,12 +260,14 @@ class Absence(commands.Cog):
self.check_absences.start() self.check_absences.start()
@commands.slash_command(name="absence", description="Déclarer une absence staff") @commands.slash_command(name="absence", description="Déclarer une absence staff")
@check_premium_tier()
async def absence(self, interaction: disnake.ApplicationCommandInteraction, async def absence(self, interaction: disnake.ApplicationCommandInteraction,
superieur: Optional[disnake.Member] = commands.Param(None, description="Le supérieur hiérarchique à prévenir")): superieur: Optional[disnake.Member] = commands.Param(None, description="Le supérieur hiérarchique à prévenir")):
"""Déclare une absence via un formulaire""" """Déclare une absence via un formulaire"""
await interaction.response.send_modal(AbsenceModal(superieur)) await interaction.response.send_modal(AbsenceModal(superieur))
@commands.slash_command(name="absence_config", description="Configurer le système d'absence (Admin)") @commands.slash_command(name="absence_config", description="Configurer le système d'absence (Admin)")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def absence_config(self, interaction: disnake.ApplicationCommandInteraction): async def absence_config(self, interaction: disnake.ApplicationCommandInteraction):
"""Ouvre le panneau de configuration des absences""" """Ouvre le panneau de configuration des absences"""
@ -350,6 +353,7 @@ class Absence(commands.Cog):
print(f"Erreur lors du barrage du message d'absence: {e}") print(f"Erreur lors du barrage du message d'absence: {e}")
@commands.slash_command(name="fin_absence", description="Terminer son absence staff") @commands.slash_command(name="fin_absence", description="Terminer son absence staff")
@check_premium_tier()
async def fin_absence(self, interaction: disnake.ApplicationCommandInteraction): async def fin_absence(self, interaction: disnake.ApplicationCommandInteraction):
"""Met fin à l'absence en cours""" """Met fin à l'absence en cours"""
all_abs = await load_absences() all_abs = await load_absences()
@ -382,6 +386,7 @@ class Absence(commands.Cog):
await interaction.response.send_message("✅ Ton absence a été retirée.", ephemeral=True) await interaction.response.send_message("✅ Ton absence a été retirée.", ephemeral=True)
@commands.slash_command(name="liste_absences", description="Afficher la liste des absences staff en cours") @commands.slash_command(name="liste_absences", description="Afficher la liste des absences staff en cours")
@check_premium_tier()
async def liste_absences(self, interaction: disnake.ApplicationCommandInteraction): async def liste_absences(self, interaction: disnake.ApplicationCommandInteraction):
"""Affiche les absences en cours sur le serveur""" """Affiche les absences en cours sur le serveur"""
all_abs = await load_absences() all_abs = await load_absences()

View file

@ -9,6 +9,7 @@ from typing import Dict, Any, List
import disnake import disnake
from disnake.ext import commands from disnake.ext import commands
from utils.premium import check_premium_tier
BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes
@ -64,6 +65,7 @@ class Backup(commands.Cog):
self.bot = bot self.bot = bot
@commands.slash_command(name="backup", description="Outils de sauvegarde du serveur") @commands.slash_command(name="backup", description="Outils de sauvegarde du serveur")
@check_premium_tier()
async def backup_group(self, interaction: disnake.ApplicationCommandInteraction): async def backup_group(self, interaction: disnake.ApplicationCommandInteraction):
pass pass

View file

@ -7,6 +7,7 @@ import shutil
from typing import List from typing import List
import disnake import disnake
from disnake.ext import commands from disnake.ext import commands
from utils.premium import check_premium_tier
BACKUPS_ROOT = "data/security_backups" BACKUPS_ROOT = "data/security_backups"
@ -133,6 +134,7 @@ class BackupRestore(commands.Cog):
await channel.send(msg["content"]) await channel.send(msg["content"])
@commands.slash_command(name="granulaire", description="Restaure un élément précis d'une sauvegarde") @commands.slash_command(name="granulaire", description="Restaure un élément précis d'une sauvegarde")
@check_premium_tier()
async def restore_granulaire( async def restore_granulaire(
self, self,
interaction: disnake.ApplicationCommandInteraction, interaction: disnake.ApplicationCommandInteraction,

View file

@ -4,6 +4,7 @@ import json
import os import os
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
from utils.premium import check_premium_tier
BLACKLIST_FILE = "data/blacklist.json" BLACKLIST_FILE = "data/blacklist.json"
AGENTS_FILE = "data/agents.json" AGENTS_FILE = "data/agents.json"
@ -57,6 +58,7 @@ class Blacklist(commands.Cog):
return str(user_id) in agents["agents"] return str(user_id) in agents["agents"]
@commands.slash_command(name="blacklist", description="Blacklist un utilisateur") @commands.slash_command(name="blacklist", description="Blacklist un utilisateur")
@check_premium_tier()
async def blacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User, reason: str = "Aucune raison fournie"): async def blacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User, reason: str = "Aucune raison fournie"):
if not self.is_agent(interaction.user.id): if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True) return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)
@ -122,6 +124,7 @@ class Blacklist(commands.Cog):
) )
@commands.slash_command(name="unblacklist", description="Retirer un utilisateur de la blacklist") @commands.slash_command(name="unblacklist", description="Retirer un utilisateur de la blacklist")
@check_premium_tier()
async def unblacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User): async def unblacklist(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.User):
if not self.is_agent(interaction.user.id): if not self.is_agent(interaction.user.id):
return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True) return await interaction.response.send_message("⛔ Tu nas pas la permission.", ephemeral=True)

View file

@ -7,6 +7,7 @@ import json
import os import os
import asyncio import asyncio
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -373,11 +374,13 @@ class BugReport(commands.Cog):
return web.json_response({"status": "error", "message": str(e)}, status=500) return web.json_response({"status": "error", "message": str(e)}, status=500)
@commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs") @commands.slash_command(name="signaler_bug", description="Signaler un bug aux développeurs")
@check_premium_tier()
async def signifier_bug(self, interaction: disnake.ApplicationCommandInteraction): async def signifier_bug(self, interaction: disnake.ApplicationCommandInteraction):
modal = BugReportModal(self) modal = BugReportModal(self)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
@commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité") @commands.slash_command(name="suggerer_fonctionnalite", description="Proposer une nouvelle fonctionnalité")
@check_premium_tier()
async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction): async def suggerer_fonctionnalite(self, interaction: disnake.ApplicationCommandInteraction):
modal = FeatureSuggestionModal(self) modal = FeatureSuggestionModal(self)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)

View file

@ -6,6 +6,7 @@ from typing import List, Optional, Dict
import datetime import datetime
import asyncio import asyncio
import logging import logging
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
@ -61,6 +62,7 @@ class Convocation(commands.Cog):
save_json(CONV_BACKUPS_FILE, self.backups) save_json(CONV_BACKUPS_FILE, self.backups)
@commands.slash_command(name="convocation", description="Gère les convocations de membres") @commands.slash_command(name="convocation", description="Gère les convocations de membres")
@check_premium_tier()
@commands.has_permissions(manage_roles=True) @commands.has_permissions(manage_roles=True)
async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction): async def convocation_group(self, interaction: disnake.ApplicationCommandInteraction):
pass pass

View file

@ -7,6 +7,7 @@ import os
import logging import logging
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from datetime import datetime, timezone from datetime import datetime, timezone
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
@ -285,6 +286,7 @@ class Goodbye(commands.Cog):
return os.path.basename(banner_path) return os.path.basename(banner_path)
@commands.slash_command(name="setgoodbye", description="Définit le salon d'au revoir et les paramètres personnalisés") @commands.slash_command(name="setgoodbye", description="Définit le salon d'au revoir et les paramètres personnalisés")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def set_goodbye(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, server_name: str = None, message: str = None, banner_file: disnake.Attachment = None): async def set_goodbye(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, server_name: str = None, message: str = None, banner_file: disnake.Attachment = None):
# ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images) # ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images)

View file

@ -5,6 +5,7 @@ import os
import logging import logging
from typing import Dict, Optional from typing import Dict, Optional
import asyncio import asyncio
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
@ -234,6 +235,7 @@ class Invites(commands.Cog):
kuby_logger.error(f"[Invites] Erreur lors de la suppression de l'enregistrement de l'invitation pour {user.name}: {e}") kuby_logger.error(f"[Invites] Erreur lors de la suppression de l'enregistrement de l'invitation pour {user.name}: {e}")
@commands.slash_command(name="setinvitechannel", description="Définit le salon des logs d'invitations") @commands.slash_command(name="setinvitechannel", description="Définit le salon des logs d'invitations")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def set_invite_channel(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel): async def set_invite_channel(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel):
try: try:

View file

@ -5,6 +5,7 @@ import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
import re import re
from src.logger import kuby_logger from src.logger import kuby_logger
from utils.premium import check_premium_tier
class ModReasonModal(disnake.ui.Modal): class ModReasonModal(disnake.ui.Modal):
def __init__(self, cog, action_type, member): def __init__(self, cog, action_type, member):
@ -254,6 +255,7 @@ class Moderation(commands.Cog):
await interaction.response.send_message(components=components, ephemeral=True) await interaction.response.send_message(components=components, ephemeral=True)
@commands.slash_command(name="modpanel", description="Ouvre le panel de modération") @commands.slash_command(name="modpanel", description="Ouvre le panel de modération")
@check_premium_tier()
@commands.has_permissions(moderate_members=True) @commands.has_permissions(moderate_members=True)
async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member): async def modpanel(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member):
if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id: if member.top_role >= interaction.user.top_role and interaction.user.id != interaction.guild.owner_id:
@ -291,6 +293,7 @@ class Moderation(commands.Cog):
await interaction.response.send_message(components=components, ephemeral=True) await interaction.response.send_message(components=components, ephemeral=True)
@commands.slash_command(name="sanctions", description="Voir l'historique des sanctions d'un membre") @commands.slash_command(name="sanctions", description="Voir l'historique des sanctions d'un membre")
@check_premium_tier()
@commands.has_permissions(moderate_members=True) @commands.has_permissions(moderate_members=True)
async def sanctions(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member): async def sanctions(self, interaction: disnake.ApplicationCommandInteraction, member: disnake.Member):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() conn = sqlite3.connect(self.db_path); cursor = conn.cursor()
@ -316,6 +319,7 @@ class Moderation(commands.Cog):
await interaction.response.send_message(components=components, ephemeral=True) await interaction.response.send_message(components=components, ephemeral=True)
@commands.slash_command(name="modconfig", description="Configuration de la modération") @commands.slash_command(name="modconfig", description="Configuration de la modération")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def modconfig(self, interaction: disnake.ApplicationCommandInteraction): async def modconfig(self, interaction: disnake.ApplicationCommandInteraction):
conn = sqlite3.connect(self.db_path); cursor = conn.cursor() conn = sqlite3.connect(self.db_path); cursor = conn.cursor()

View file

@ -10,6 +10,7 @@ except ImportError:
import disnake import disnake
from disnake.ext import commands, tasks from disnake.ext import commands, tasks
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
PARIS_TZ = ZoneInfo("Europe/Paris") PARIS_TZ = ZoneInfo("Europe/Paris")
@ -906,6 +907,7 @@ class Rapports(commands.Cog):
await self.bot.wait_until_ready() await self.bot.wait_until_ready()
@commands.slash_command(name="config-system", description="Configuration du module de rapports (Admin uniquement)") @commands.slash_command(name="config-system", description="Configuration du module de rapports (Admin uniquement)")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def config_system(self, interaction: disnake.ApplicationCommandInteraction, role_viewer: disnake.Role = None): async def config_system(self, interaction: disnake.ApplicationCommandInteraction, role_viewer: disnake.Role = None):
guild_id = interaction.guild.id guild_id = interaction.guild.id
@ -938,10 +940,12 @@ class Rapports(commands.Cog):
await interaction.response.send_message(embed=embed, view=view, ephemeral=True) await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
@commands.slash_command(name="rapport", description="Ouvrir le formulaire de saisie de rapport") @commands.slash_command(name="rapport", description="Ouvrir le formulaire de saisie de rapport")
@check_premium_tier()
async def rapport(self, interaction: disnake.ApplicationCommandInteraction): async def rapport(self, interaction: disnake.ApplicationCommandInteraction):
await interaction.response.send_modal(ReportModal(self)) await interaction.response.send_modal(ReportModal(self))
@commands.slash_command(name="consulter_rapport", description="Consulter les rapports du serveur via une interface interactive") @commands.slash_command(name="consulter_rapport", description="Consulter les rapports du serveur via une interface interactive")
@check_premium_tier()
async def consulter_rapport( async def consulter_rapport(
self, self,
interaction: disnake.ApplicationCommandInteraction, interaction: disnake.ApplicationCommandInteraction,

View file

@ -5,6 +5,7 @@ import hashlib
import os import os
import json import json
import re import re
from utils.premium import check_premium_tier
MALWARE_DB = "malware.json" MALWARE_DB = "malware.json"
SUSPICIOUS_EXTENSIONS = [".exe", ".scr", ".bat", ".js", ".vbs", ".cmd"] SUSPICIOUS_EXTENSIONS = [".exe", ".scr", ".bat", ".js", ".vbs", ".cmd"]
@ -59,6 +60,7 @@ class Scan(commands.Cog):
self.bot = bot self.bot = bot
@commands.slash_command(name="scan", description="Scanner un fichier pour détecter un malware.") @commands.slash_command(name="scan", description="Scanner un fichier pour détecter un malware.")
@check_premium_tier()
async def scan(self, interaction: disnake.ApplicationCommandInteraction, file: disnake.Attachment): async def scan(self, interaction: disnake.ApplicationCommandInteraction, file: disnake.Attachment):
await interaction.response.defer() # Permet de prendre un peu de temps pour le scan await interaction.response.defer() # Permet de prendre un peu de temps pour le scan

View file

@ -3,6 +3,7 @@ from disnake.ext import commands
import json import json
import os import os
from typing import Optional from typing import Optional
from utils.premium import check_premium_tier
# Chemins ABSOLUS des fichiers (basés sur le fichier courant) # Chemins ABSOLUS des fichiers (basés sur le fichier courant)
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -1029,6 +1030,7 @@ class Security(commands.Cog):
name="security", name="security",
description="🛡️ Ouvrir le panneau de sécurité Kuby" description="🛡️ Ouvrir le panneau de sécurité Kuby"
) )
@check_premium_tier()
async def security(self, interaction: disnake.ApplicationCommandInteraction): async def security(self, interaction: disnake.ApplicationCommandInteraction):
""" """
Commande principale pour ouvrir l'interface de sécurité. Commande principale pour ouvrir l'interface de sécurité.

View file

@ -1,5 +1,6 @@
import disnake import disnake
from disnake.ext import commands from disnake.ext import commands
from utils.premium import check_premium_tier
OWNER_ID = 971446412690722826 OWNER_ID = 971446412690722826
@ -124,6 +125,7 @@ class SentBotMessages(commands.Cog):
name="sentbotmessages", name="sentbotmessages",
description="Le bot envoie un message dans un salon choisi." description="Le bot envoie un message dans un salon choisi."
) )
@check_premium_tier()
async def sentbotmessages( async def sentbotmessages(
self, self,
interaction: disnake.ApplicationCommandInteraction, interaction: disnake.ApplicationCommandInteraction,
@ -154,6 +156,7 @@ class SentBotMessages(commands.Cog):
name="annonce_proprietaires", name="annonce_proprietaires",
description="Ouvrir le formulaire d'annonce pour les propriétaires." description="Ouvrir le formulaire d'annonce pour les propriétaires."
) )
@check_premium_tier()
@commands.check(is_authorized) @commands.check(is_authorized)
async def annonce_proprietaires( async def annonce_proprietaires(
self, self,

View file

@ -2,6 +2,7 @@ import disnake
from disnake.ext import commands from disnake.ext import commands
from datetime import datetime from datetime import datetime
from collections import deque from collections import deque
from utils.premium import check_premium_tier
class SnipeCommands(commands.Cog): class SnipeCommands(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
@ -34,6 +35,7 @@ class SnipeCommands(commands.Cog):
self.deleted_messages[channel_id].appendleft(message_data) self.deleted_messages[channel_id].appendleft(message_data)
@commands.slash_command(name="snipe", description="Voir les derniers messages supprimés d'un salon") @commands.slash_command(name="snipe", description="Voir les derniers messages supprimés d'un salon")
@check_premium_tier()
async def snipe(self, interaction: disnake.ApplicationCommandInteraction, async def snipe(self, interaction: disnake.ApplicationCommandInteraction,
nombre: int = commands.Param(1, description="Nombre de messages à afficher (1-10, par défaut: 1)")): nombre: int = commands.Param(1, description="Nombre de messages à afficher (1-10, par défaut: 1)")):
"""Retrieve deleted messages from the current channel""" """Retrieve deleted messages from the current channel"""
@ -93,6 +95,7 @@ class SnipeCommands(commands.Cog):
await interaction.response.send_message(embed=embed) await interaction.response.send_message(embed=embed)
@commands.slash_command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon") @commands.slash_command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon")
@check_premium_tier()
async def snipeclear(self, interaction: disnake.ApplicationCommandInteraction): async def snipeclear(self, interaction: disnake.ApplicationCommandInteraction):
"""Clear the deleted messages history for the current channel""" """Clear the deleted messages history for the current channel"""
channel_id = interaction.channel.id channel_id = interaction.channel.id

View file

@ -4,6 +4,7 @@ from datetime import datetime
from commandes.ticket.data.storage import get_storage from commandes.ticket.data.storage import get_storage
from commandes.ticket.staff_analytics import StaffAnalytics from commandes.ticket.staff_analytics import StaffAnalytics
from utils.premium import check_premium_tier
class StaffLeaderboard(commands.Cog): class StaffLeaderboard(commands.Cog):
@ -11,6 +12,7 @@ class StaffLeaderboard(commands.Cog):
self.bot = bot self.bot = bot
@commands.slash_command(name="staff-leaderboard", description="Affiche le classement du staff") @commands.slash_command(name="staff-leaderboard", description="Affiche le classement du staff")
@check_premium_tier()
async def staff_leaderboard(self, interaction: disnake.ApplicationCommandInteraction): async def staff_leaderboard(self, interaction: disnake.ApplicationCommandInteraction):
"""Commande pour afficher le classement du staff""" """Commande pour afficher le classement du staff"""
await interaction.response.defer(ephemeral=False) await interaction.response.defer(ephemeral=False)

View file

@ -12,6 +12,7 @@ from collections import defaultdict
from commandes.ticket.data.storage import get_storage from commandes.ticket.data.storage import get_storage
from commandes.ticket.data.models import TicketStatus from commandes.ticket.data.models import TicketStatus
from commandes.ticket.staff_analytics import StaffAnalytics from commandes.ticket.staff_analytics import StaffAnalytics
from utils.premium import check_premium_tier
class StaffRatings(commands.Cog): class StaffRatings(commands.Cog):
@ -19,6 +20,7 @@ class StaffRatings(commands.Cog):
self.bot = bot self.bot = bot
@commands.slash_command(name="staff-ratings", description="Affiche les évaluations du staff") @commands.slash_command(name="staff-ratings", description="Affiche les évaluations du staff")
@check_premium_tier()
async def staff_ratings(self, interaction: disnake.ApplicationCommandInteraction, async def staff_ratings(self, interaction: disnake.ApplicationCommandInteraction,
team: str = commands.Param(None, description="Filtrer par équipe (optionnel)")): team: str = commands.Param(None, description="Filtrer par équipe (optionnel)")):
"""Commande pour afficher les évaluations du staff""" """Commande pour afficher les évaluations du staff"""

View file

@ -18,6 +18,7 @@ from utils.gestion_taches import (
PRIORITES, STATUTS PRIORITES, STATUTS
) )
from src.logger import kuby_logger from src.logger import kuby_logger
from utils.premium import check_premium_tier
# ============================================================================ # ============================================================================
@ -655,6 +656,7 @@ class TachesCog(commands.Cog):
# ========================================================================== # ==========================================================================
@commands.slash_command(name="taches", description="Ouvrir le menu de gestion des tâches") @commands.slash_command(name="taches", description="Ouvrir le menu de gestion des tâches")
@check_premium_tier()
async def taches_command(self, inter: disnake.ApplicationCommandInteraction): async def taches_command(self, inter: disnake.ApplicationCommandInteraction):
"""Commande principale pour accéder au menu des tâches.""" """Commande principale pour accéder au menu des tâches."""
settings = await get_taches_settings(inter.guild.id) settings = await get_taches_settings(inter.guild.id)

View file

@ -162,6 +162,7 @@ class TeamJSON(commands.Cog):
name="generate_team_json", name="generate_team_json",
description="Génère le JSON de l'équipe (admin only)" description="Génère le JSON de l'équipe (admin only)"
) )
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def generate_team_json(self, inter: disnake.ApplicationCommandInteraction): async def generate_team_json(self, inter: disnake.ApplicationCommandInteraction):
"""Commande manuelle pour (re)générer le JSON et les avatars.""" """Commande manuelle pour (re)générer le JSON et les avatars."""

View file

@ -38,6 +38,7 @@ from .utils.formatters import TicketEmbedFormatter
from .staff_analytics import StaffAnalytics, StaffStatsView from .staff_analytics import StaffAnalytics, StaffStatsView
from .utils.permissions import is_staff, is_whitelisted, can_access_config from .utils.permissions import is_staff, is_whitelisted, can_access_config
from src.logger import kuby_logger from src.logger import kuby_logger
from utils.premium import check_premium_tier
# V2 RICH COMPONENTS LOADED - 14:10 # V2 RICH COMPONENTS LOADED - 14:10
kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...") kuby_logger.info("🎫 [TicketSystem] Chargement de la version V2 Rich Components...")
@ -3951,15 +3952,18 @@ class TicketCommands(commands.Cog):
await inter.followup.send("❌ Erreur lors de la decision de recrutement.", ephemeral=True) await inter.followup.send("❌ Erreur lors de la decision de recrutement.", ephemeral=True)
# === COMMANDES === # === COMMANDES ===
@commands.slash_command(name="ticket", description="Ouvre le panel des tickets") @commands.slash_command(name="ticket", description="Ouvre le panel des tickets")
@check_premium_tier()
async def ticket_main(self, inter: disnake.ApplicationCommandInteraction): async def ticket_main(self, inter: disnake.ApplicationCommandInteraction):
"""Ouvre le panel des tickets""" """Ouvre le panel des tickets"""
config = self.storage.load_config(inter.guild.id) config = self.storage.load_config(inter.guild.id)
view = TicketPanelView(self.bot, config, self.storage) view = TicketPanelView(self.bot, config, self.storage)
await inter.response.send_message(components=view.get_components_v2(), ephemeral=True) await inter.response.send_message(components=view.get_components_v2(), ephemeral=True)
@commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal") @commands.slash_command(name="ticketpanel", description="Envoie le panel de création de tickets dans un canal")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def ticket_panel(self, inter: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel = None): async def ticket_panel(self, inter: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel = None):
"""Envoie le panel de création de tickets dans un canal""" """Envoie le panel de création de tickets dans un canal"""
@ -3983,6 +3987,7 @@ class TicketCommands(commands.Cog):
await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}") await inter.edit_original_response(content=f"Panel envoyé dans {target.mention}")
@commands.slash_command(name="ticketconfig", description="Ouvre le panel de configuration complet du système de tickets") @commands.slash_command(name="ticketconfig", description="Ouvre le panel de configuration complet du système de tickets")
@check_premium_tier()
async def ticket_config(self, inter: disnake.ApplicationCommandInteraction): async def ticket_config(self, inter: disnake.ApplicationCommandInteraction):
"""Ouvre le panel de configuration complet du système de tickets""" """Ouvre le panel de configuration complet du système de tickets"""
await inter.response.defer(ephemeral=True) await inter.response.defer(ephemeral=True)

View file

@ -9,6 +9,7 @@ from typing import Optional
from src.logger import kuby_logger from src.logger import kuby_logger
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from utils.premium import check_premium_tier
# Dictionnaire global pour suivre les renommages de salons # Dictionnaire global pour suivre les renommages de salons
# Format: {channel_id: [timestamp1, timestamp2, ...]} # Format: {channel_id: [timestamp1, timestamp2, ...]}
@ -1586,6 +1587,7 @@ class Ticket(commands.Cog):
self.bot.add_view(ClosedTicketView()) self.bot.add_view(ClosedTicketView())
@commands.slash_command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.") @commands.slash_command(name="ticket_whitelist", description="Envoie l'embed pour ouvrir un ticket de whitelist.")
@check_premium_tier()
async def ticket_whitelist(self, interaction: disnake.ApplicationCommandInteraction): async def ticket_whitelist(self, interaction: disnake.ApplicationCommandInteraction):
"""Envoie l'embed avec le bouton d'ouverture de ticket""" """Envoie l'embed avec le bouton d'ouverture de ticket"""
if not is_staff_or_admin(interaction.user, interaction.guild): if not is_staff_or_admin(interaction.user, interaction.guild):
@ -1601,6 +1603,7 @@ class Ticket(commands.Cog):
await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True) await interaction.response.send_message("✅ Embed des tickets envoyé.", ephemeral=True)
@commands.slash_command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Staff/Admin)") @commands.slash_command(name="ticket_whitelist_config", description="Configurer le système de tickets whitelist (Staff/Admin)")
@check_premium_tier()
async def ticket_whitelist_config(self, interaction: disnake.ApplicationCommandInteraction): async def ticket_whitelist_config(self, interaction: disnake.ApplicationCommandInteraction):
"""Interface de configuration des tickets en Containers V2""" """Interface de configuration des tickets en Containers V2"""
if not is_staff_or_admin(interaction.user, interaction.guild): if not is_staff_or_admin(interaction.user, interaction.guild):

View file

@ -7,6 +7,7 @@ import os
import logging import logging
import asyncio import asyncio
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from utils.premium import check_premium_tier
kuby_logger = logging.getLogger("KubyBot") kuby_logger = logging.getLogger("KubyBot")
@ -345,6 +346,7 @@ class Welcome(commands.Cog):
return os.path.basename(banner_path) return os.path.basename(banner_path)
@commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière") @commands.slash_command(name="setwelcome", description="Définit le salon, les messages de bienvenue et le fond de la bannière")
@check_premium_tier()
@commands.has_permissions(administrator=True) @commands.has_permissions(administrator=True)
async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: disnake.Attachment = None): async def set_welcome(self, interaction: disnake.ApplicationCommandInteraction, channel: disnake.TextChannel, message_dm: str, message_salon: str, server_name: str, banner_file: disnake.Attachment = None):
# ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images) # ✅ DÉFÉRER LA RÉPONSE (le traitement peut prendre > 3s avec les images)
@ -385,6 +387,7 @@ class Welcome(commands.Cog):
await interaction.followup.send(error_message, ephemeral=True) await interaction.followup.send(error_message, ephemeral=True)
@commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations") @commands.slash_command(name="invitations", description="Affiche vos statistiques d'invitations")
@check_premium_tier()
async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None): async def invitations_stats(self, interaction: disnake.ApplicationCommandInteraction, user: disnake.Member = None):
user = user or interaction.author user = user or interaction.author
await self._send_invitation_stats(interaction, user) await self._send_invitation_stats(interaction, user)

View file

@ -3,6 +3,7 @@ from disnake.ext import commands
import json import json
import os import os
from typing import List from typing import List
from utils.premium import check_premium_tier
# Chemins ABSOLUS des fichiers # Chemins ABSOLUS des fichiers
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -38,6 +39,7 @@ class Whitelist(commands.Cog):
self.save_whitelist() self.save_whitelist()
@commands.slash_command(name="whitelist", description="Gère la whitelist du serveur") @commands.slash_command(name="whitelist", description="Gère la whitelist du serveur")
@check_premium_tier()
async def whitelist_group(self, interaction: disnake.ApplicationCommandInteraction): async def whitelist_group(self, interaction: disnake.ApplicationCommandInteraction):
"""Groupe de commandes pour gérer la whitelist""" """Groupe de commandes pour gérer la whitelist"""
pass pass

210
test_paypal_logic.py Normal file
View file

@ -0,0 +1,210 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
import os
import json
import asyncio
# On injecte les vrais IDs à 18 chiffres de ton .env pour le test
os.environ["STARTER_SKU_ID"] = "111111111111111111"
os.environ["MEDIUM_SKU_ID"] = "222222222222222222"
os.environ["LARGE_SKU_ID"] = "333333333333333333"
from utils.premium import load_partners_whitelist, get_sku_ids, check_premium_tier, WHITELIST_PARTNERS_FILE
class TestPremiumSystem(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Nettoyage du fichier JSON avant chaque test
if os.path.exists(WHITELIST_PARTNERS_FILE):
try:
os.remove(WHITELIST_PARTNERS_FILE)
except Exception:
pass
def tearDown(self):
# Nettoyage du fichier JSON après chaque test
if os.path.exists(WHITELIST_PARTNERS_FILE):
try:
os.remove(WHITELIST_PARTNERS_FILE)
except Exception:
pass
async def test_get_sku_ids(self):
"""Vérifie que la lecture du .env convertit bien les chaînes en nombres entiers (int)"""
starter, medium, large = get_sku_ids()
self.assertEqual(starter, 111111111111111111)
self.assertEqual(medium, 222222222222222222)
self.assertEqual(large, 333333333333333333)
async def test_load_partners_whitelist_empty(self):
"""Vérifie que le système gère proprement un fichier whitelist vide sans crasher"""
partners = await load_partners_whitelist()
self.assertEqual(partners, [])
self.assertTrue(os.path.exists(WHITELIST_PARTNERS_FILE))
async def test_load_partners_whitelist_with_data(self):
"""Vérifie le chargement des partenaires depuis le fichier JSON"""
with open(WHITELIST_PARTNERS_FILE, "w") as f:
json.dump({"partners": [123, 456]}, f)
partners = await load_partners_whitelist()
self.assertEqual(partners, [123, 456])
async def test_check_premium_tier_dm_fails(self):
"""Vérifie que le bot bloque proprement les commandes exécutées en DM (hors serveur)"""
interaction = MagicMock()
interaction.guild = None
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once_with(
"❌ Cette commande ne peut être exécutée que sur un serveur Discord.",
ephemeral=True
)
async def test_check_premium_tier_whitelist_passes(self):
"""Vérifie qu'un serveur dans la Whitelist passe sans condition, même sans SKU"""
with open(WHITELIST_PARTNERS_FILE, "w") as f:
json.dump({"partners": [12345]}, f)
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 12345
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
interaction.response.send_message.assert_not_called()
async def test_check_premium_tier_invalid_member_count_fails(self):
"""Vérifie la sécurité (Fail-Safe) si le nombre de membres Discord renvoie une erreur"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = None
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once_with(
"❌ Impossible de vérifier la taille du serveur par sécurité. Veuillez réessayer plus tard.",
ephemeral=True
)
async def test_check_premium_tier_less_than_500_with_starter(self):
"""Vérifie qu'un petit serveur (<500 membres) avec le SKU Starter est bien AUTORISÉ"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 100
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 111111111111111111
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_500_to_2500_with_starter_only_fails(self):
"""Vérifie qu'un serveur moyen qui a grandi (>500 membres) est BLOQUÉ s'il n'a qu'un SKU Starter"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 1000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 111111111111111111
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once()
self.assertIn("Palier Medium", interaction.response.send_message.call_args[0][0])
async def test_check_premium_tier_500_to_2500_with_medium_passes(self):
"""Vérifie qu'un serveur moyen (1000 membres) avec le SKU Medium est bien AUTORISÉ"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 1000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 222222222222222222
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_above_2500_with_large_passes(self):
"""Vérifie qu'un très gros serveur (>2500 membres) avec le SKU Large est bien AUTORISÉ"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 3000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 333333333333333333
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_above_2500_with_medium_fails(self):
"""Vérifie qu'un très gros serveur (>2500 membres) est BLOQUÉ s'il n'a qu'un SKU Medium"""
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 3000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 222222222222222222
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
self.assertIn("Palier Large", interaction.response.send_message.call_args[0][0])
if __name__ == "__main__":
unittest.main()

200
test_premium.py Normal file
View file

@ -0,0 +1,200 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
import os
import json
import asyncio
# Setup env variables before importing premium module
os.environ["STARTER_SKU_ID"] = "111111"
os.environ["MEDIUM_SKU_ID"] = "222222"
os.environ["LARGE_SKU_ID"] = "333333"
from utils.premium import load_partners_whitelist, get_sku_ids, check_premium_tier, WHITELIST_PARTNERS_FILE
class TestPremiumSystem(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Reset whitelist file before each test
if os.path.exists(WHITELIST_PARTNERS_FILE):
try:
os.remove(WHITELIST_PARTNERS_FILE)
except Exception:
pass
def tearDown(self):
# Clean up whitelist file
if os.path.exists(WHITELIST_PARTNERS_FILE):
try:
os.remove(WHITELIST_PARTNERS_FILE)
except Exception:
pass
async def test_get_sku_ids(self):
starter, medium, large = get_sku_ids()
self.assertEqual(starter, 111111)
self.assertEqual(medium, 222222)
self.assertEqual(large, 333333)
async def test_load_partners_whitelist_empty(self):
partners = await load_partners_whitelist()
self.assertEqual(partners, [])
self.assertTrue(os.path.exists(WHITELIST_PARTNERS_FILE))
async def test_load_partners_whitelist_with_data(self):
with open(WHITELIST_PARTNERS_FILE, "w") as f:
json.dump({"partners": [123, 456]}, f)
partners = await load_partners_whitelist()
self.assertEqual(partners, [123, 456])
async def test_check_premium_tier_dm_fails(self):
interaction = MagicMock()
interaction.guild = None
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once_with(
"❌ Cette commande ne peut être exécutée que sur un serveur Discord.",
ephemeral=True
)
async def test_check_premium_tier_whitelist_passes(self):
with open(WHITELIST_PARTNERS_FILE, "w") as f:
json.dump({"partners": [12345]}, f)
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 12345
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
interaction.response.send_message.assert_not_called()
async def test_check_premium_tier_invalid_member_count_fails(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = None
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once_with(
"❌ Impossible de vérifier la taille du serveur par sécurité. Veuillez réessayer plus tard.",
ephemeral=True
)
async def test_check_premium_tier_less_than_500_with_starter(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 100
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
# Mock entitlements
entitlement = MagicMock()
entitlement.sku_id = 111111
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_500_to_2500_with_starter_only_fails(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 1000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 111111
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
interaction.response.send_message.assert_called_once()
self.assertIn("Palier Medium", interaction.response.send_message.call_args[0][0])
async def test_check_premium_tier_500_to_2500_with_medium_passes(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 1000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 222222
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_above_2500_with_large_passes(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 3000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 333333
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertTrue(result)
async def test_check_premium_tier_above_2500_with_medium_fails(self):
interaction = MagicMock()
interaction.guild = MagicMock()
interaction.guild.id = 999
interaction.guild.member_count = 3000
interaction.response = MagicMock()
interaction.response.send_message = AsyncMock()
entitlement = MagicMock()
entitlement.sku_id = 222222
entitlement.is_active.return_value = True
interaction.entitlements = [entitlement]
decorator = check_premium_tier()
predicate = decorator.predicate
result = await predicate(interaction)
self.assertFalse(result)
self.assertIn("Palier Large", interaction.response.send_message.call_args[0][0])
if __name__ == "__main__":
unittest.main()

View file

@ -1,60 +0,0 @@
#!/usr/bin/env python3
"""
Test script to verify the staff ratings implementation
"""
import sys
import os
# Add the project root to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def test_imports():
"""Test that all modules can be imported correctly"""
try:
from commandes.ticket.staff_analytics import StaffAnalytics
print("✓ StaffAnalytics imported successfully")
from commandes.ticket.feedback_view import FeedbackView
print("✓ FeedbackView imported successfully")
from commandes.ticket import TicketPanelView
print("✓ TicketPanelView imported successfully")
from commandes.staff_ratings import StaffRatings
print("✓ StaffRatings imported successfully")
from commandes.staff_leaderboard import StaffLeaderboard
print("✓ StaffLeaderboard imported successfully")
return True
except Exception as e:
print(f"✗ Import error: {e}")
return False
def test_requirements():
"""Test that required packages are available"""
try:
import matplotlib
import plotly
print("✓ matplotlib and plotly imported successfully")
return True
except Exception as e:
print(f"✗ Package import error: {e}")
return False
if __name__ == "__main__":
print("Testing staff ratings implementation...")
print("=" * 40)
success = True
success &= test_imports()
success &= test_requirements()
print("=" * 40)
if success:
print("✓ All tests passed!")
sys.exit(0)
else:
print("✗ Some tests failed!")
sys.exit(1)

205
utils/premium.py Normal file
View 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)

5
whitelist_parteners.json Normal file
View file

@ -0,0 +1,5 @@
{
"partners": [
1369669999345537145
]
}

5
whitelist_partners.json Normal file
View file

@ -0,0 +1,5 @@
{
"partners": [
1369669999345537145
]
}