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
24
TODO.md
24
TODO.md
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 n’as pas la permission.", ephemeral=True)
|
return await interaction.response.send_message("⛔ Tu n’as 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 n’as pas la permission.", ephemeral=True)
|
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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é.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
210
test_paypal_logic.py
Normal 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
200
test_premium.py
Normal 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()
|
||||||
|
|
@ -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
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)
|
||||||
5
whitelist_parteners.json
Normal file
5
whitelist_parteners.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"partners": [
|
||||||
|
1369669999345537145
|
||||||
|
]
|
||||||
|
}
|
||||||
5
whitelist_partners.json
Normal file
5
whitelist_partners.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"partners": [
|
||||||
|
1369669999345537145
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue