Cours Vibe coding avec l'IA · Module 9: Clone de Discord
Rôles, permissions et modération
Dans cette leçon3
Barbe-Noire s'est glissé sur le serveur de la guilde et inonde le salon des annonces de spam. Un simple membre ne peut rien faire, le capitaine est absent, et le second ignore s'il peut expulser quelqu'un. Le serveur a besoin de règles claires : qui peut écrire, supprimer des messages, expulser et bannir. Discord les enregistre sous forme de permissions binaires (bitwise) avec une hiérarchie de rôles et des surcharges par salon (channel overrides).
Les permissions sous forme de bits
Chaque permission est un bit dans un nombre. On crée la valeur avec un décalage de bits, par exemple KICK_MEMBERS = 1 << 1 ou ADMINISTRATOR = 1 << 3 :
1// Permissions bit à bit comme dans Discord
2enum PermissionBits {
3 CREATE_INSTANT_INVITE = 1 << 0,
4 KICK_MEMBERS = 1 << 1,
5 BAN_MEMBERS = 1 << 2,
6 ADMINISTRATOR = 1 << 3,
7 MANAGE_CHANNELS = 1 << 4,
8 MANAGE_SERVER = 1 << 5,
9 ADD_REACTIONS = 1 << 6,
10 VIEW_AUDIT_LOG = 1 << 7,
11 PRIORITY_SPEAKER = 1 << 8, // vocal
12 STREAM = 1 << 9, // vocal
13 VIEW_CHANNEL = 1 << 10,
14 SEND_MESSAGES = 1 << 11,
15 SEND_TTS_MESSAGES = 1 << 12,
16 MANAGE_MESSAGES = 1 << 13,
17 EMBED_LINKS = 1 << 14,
18 ATTACH_FILES = 1 << 15,
19 READ_MESSAGE_HISTORY = 1 << 16,
20 MENTION_EVERYONE = 1 << 17,
21 USE_EXTERNAL_EMOJIS = 1 << 18,
22 CONNECT = 1 << 20, // vocal
23 SPEAK = 1 << 21, // vocal
24 MUTE_MEMBERS = 1 << 22, // vocal
25 DEAFEN_MEMBERS = 1 << 23, // vocal
26 MOVE_MEMBERS = 1 << 24, // vocal
27 USE_VAD = 1 << 25, // détection d'activité vocale
28 CHANGE_NICKNAME = 1 << 26,
29 MANAGE_NICKNAMES = 1 << 27,
30 MANAGE_ROLES = 1 << 28,
31 MANAGE_WEBHOOKS = 1 << 29,
32 MANAGE_EMOJIS = 1 << 30,
33}
34
35// Vérifie si l'utilisateur a la permission
36function hasPermission(
37 member: Member,
38 permission: PermissionBits,
39 roles: Role[],
40 channel?: Channel
41): boolean {
42 // Le propriétaire a toutes les permissions
43 if (member.isOwner) return true;
44
45 // Récupérer tous les rôles du membre
46 const memberRoles = roles.filter(r => member.roles.includes(r.id));
47
48 // Vérifier la présence d'ADMINISTRATOR
49 if (memberRoles.some(r => r.permissions & PermissionBits.ADMINISTRATOR)) {
50 return true;
51 }
52
53 // Calculer les permissions de base à partir des rôles
54 let permissions = 0;
55 memberRoles.forEach(role => {
56 permissions |= role.permissions;
57 });
58
59 // Appliquer les surcharges spécifiques au salon
60 if (channel) {
61 channel.permissions.forEach(override => {
62 if (member.roles.includes(override.roleId)) {
63 // Appliquer d'abord les refus
64 override.deny.forEach(perm => {
65 permissions &= ~perm;
66 });
67 // Puis appliquer les autorisations
68 override.allow.forEach(perm => {
69 permissions |= perm;
70 });
71 }
72 });
73 }
74
75 return (permissions & permission) === permission;
76}1 << 3 décale le un de trois places vers la gauche, ce qui donne 1000 en binaire, soit 8. Un rôle avec plusieurs permissions a simplement plusieurs bits allumés. Dans cette leçon, role.permissions est donc un nombre, et non un tableau de noms comme dans la leçon sur les serveurs.
hasPermission calcule les permissions en quatre étapes. Le propriétaire reçoit tout de suite true (dans l'exemple, le champ isOwner ; dans du vrai code, tu compares server.owner à l'id du membre). Si l'un des rôles du membre, choisis par la condition member.roles.includes(role.id), a ADMINISTRATOR, la fonction renvoie aussi true, car ADMINISTRATOR donne un accès complet à toutes les permissions. Ensuite, elle combine les permissions des rôles avec l'opérateur OR : permissions |= role.permissions. Enfin, elle applique les surcharges de salon : d'abord deny, qui retire un bit avec &= ~perm, puis allow, qui en ajoute un avec |=. Discord suit la même règle : les refus avant les autorisations.
La dernière ligne, (permissions & permission) === permission, vérifie que le bit recherché est allumé. Un appel ressemble à ceci : hasPermission(member, KICK_MEMBERS, roles).
Attention à la limite des nombres. En JavaScript, les opérateurs bit à bit travaillent sur des nombres de 32 bits, alors que Discord a déjà des permissions au-delà du bit 50, transmises sous forme de texte :
1console.log(1 << 31); // -2147483648 : nombre négatif
2console.log(1 << 32); // 1 : le décalage est revenu au début
3console.log(1n << 40n); // 1099511627776n : BigInt n'a pas cette limiteNotre enum s'arrête au bit 30, il tient donc dans la plage, mais avec la vraie API de Discord on calcule les permissions avec le type BigInt.
Le gestionnaire de rôles
Une hiérarchie typique, du rôle le plus haut : Propriétaire (ADMINISTRATOR), Administrateur (MANAGE_SERVER), Modérateur (KICK_MEMBERS) et Membre (SEND_MESSAGES). Le gestionnaire affiche les rôles sous forme de liste avec un éditeur :
1'use client';
2
3import { useState } from 'react';
4
5interface RoleManagerProps {
6 server: Server;
7 onCreateRole: () => void;
8 onEditRole: (roleId: string) => void;
9 onDeleteRole: (roleId: string) => void;
10}
11
12export default function RoleManager({
13 server,
14 onCreateRole,
15 onEditRole,
16 onDeleteRole
17}: RoleManagerProps) {
18 const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
19
20 const selectedRole = server.roles.find(r => r.id === selectedRoleId);
21
22 return (
23 <div className="flex h-screen bg-gray-800">
24 {/* Liste des rôles */}
25 <div className="w-64 bg-gray-900 p-4">
26 <div className="flex items-center justify-between mb-4">
27 <h3 className="font-bold text-white">Rôles</h3>
28 <button
29 onClick={onCreateRole}
30 className="w-8 h-8 bg-green-600 hover:bg-green-700 rounded-full text-white"
31 >
32 +
33 </button>
34 </div>
35
36 <div className="space-y-1">
37 {server.roles
38 .sort((a, b) => b.position - a.position)
39 .map(role => (
40 <button
41 key={role.id}
42 onClick={() => setSelectedRoleId(role.id)}
43 className={`w-full text-left px-3 py-2 rounded flex items-center justify-between ${
44 selectedRoleId === role.id
45 ? 'bg-gray-700'
46 : 'hover:bg-gray-800'
47 }`}
48 >
49 <div className="flex items-center gap-2 flex-1 min-w-0">
50 <div
51 className="w-3 h-3 rounded-full"
52 style={{ backgroundColor: role.color || '#99AAB5' }}
53 />
54 <span className="text-white text-sm truncate">
55 {role.name}
56 </span>
57 </div>
58 <span className="text-xs text-gray-400">
59 {role.memberCount}
60 </span>
61 </button>
62 ))}
63 </div>
64
65 <div className="mt-4 pt-4 border-t border-gray-800 text-xs text-gray-400">
66 Glisse pour réordonner les rôles
67 </div>
68 </div>
69
70 {/* Éditeur de rôle */}
71 {selectedRole ? (
72 <div className="flex-1 p-6 overflow-y-auto">
73 <div className="max-w-2xl">
74 <h2 className="text-2xl font-bold text-white mb-6">
75 Modifier le rôle : {selectedRole.name}
76 </h2>
77
78 {/* Affichage */}
79 <div className="bg-gray-700 rounded-lg p-4 mb-6">
80 <h3 className="font-bold text-white mb-3">Affichage</h3>
81
82 <div className="mb-4">
83 <label className="block text-sm text-gray-300 mb-2">
84 Nom du rôle
85 </label>
86 <input
87 type="text"
88 value={selectedRole.name}
89 className="w-full bg-gray-900 text-white px-3 py-2 rounded"
90 />
91 </div>
92
93 <div className="mb-4">
94 <label className="block text-sm text-gray-300 mb-2">
95 Couleur du rôle
96 </label>
97 <div className="flex items-center gap-3">
98 <input
99 type="color"
100 value={selectedRole.color || '#99AAB5'}
101 className="w-16 h-10 rounded cursor-pointer"
102 />
103 <span className="text-gray-400 text-sm">
104 {selectedRole.color || 'Par défaut'}
105 </span>
106 </div>
107 </div>
108
109 <div className="space-y-2">
110 <label className="flex items-center gap-2 text-sm text-gray-300">
111 <input
112 type="checkbox"
113 checked={selectedRole.isHoisted}
114 className="w-4 h-4"
115 />
116 Afficher les membres du rôle séparément des membres en ligne
117 </label>
118
119 <label className="flex items-center gap-2 text-sm text-gray-300">
120 <input
121 type="checkbox"
122 checked={selectedRole.isMentionable}
123 className="w-4 h-4"
124 />
125 Autoriser tout le monde à @mentionner ce rôle
126 </label>
127 </div>
128 </div>
129
130 {/* Permissions */}
131 <div className="bg-gray-700 rounded-lg p-4">
132 <h3 className="font-bold text-white mb-3">Permissions</h3>
133
134 <div className="space-y-3">
135 {Object.entries(PermissionBits).map(([name, bit]) => {
136 const hasPermission = (selectedRole.permissions & bit) === bit;
137
138 return (
139 <label
140 key={name}
141 className="flex items-center gap-3 text-sm text-gray-300 hover:bg-gray-600 p-2 rounded cursor-pointer"
142 >
143 <input
144 type="checkbox"
145 checked={hasPermission}
146 className="w-4 h-4"
147 />
148 <div className="flex-1">
149 <div className="font-semibold">
150 {name.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ')}
151 </div>
152 <div className="text-xs text-gray-400">
153 {getPermissionDescription(name)}
154 </div>
155 </div>
156 </label>
157 );
158 })}
159 </div>
160 </div>
161
162 {/* Actions */}
163 <div className="mt-6 flex gap-3">
164 <button
165 onClick={() => onEditRole(selectedRole.id)}
166 className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-semibold"
167 >
168 Enregistrer les modifications
169 </button>
170 <button
171 onClick={() => onDeleteRole(selectedRole.id)}
172 className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold"
173 >
174 Supprimer le rôle
175 </button>
176 </div>
177 </div>
178 </div>
179 ) : (
180 <div className="flex-1 flex items-center justify-center text-gray-500">
181 Sélectionne un rôle à modifier
182 </div>
183 )}
184 </div>
185 );
186}
187
188function getPermissionDescription(permission: string): string {
189 const descriptions: Record<string, string> = {
190 ADMINISTRATOR: 'Accès complet à tous les salons et paramètres',
191 MANAGE_SERVER: 'Changer le nom du serveur, la région et d\'autres paramètres',
192 MANAGE_ROLES: 'Créer, modifier et supprimer les rôles inférieurs à celui-ci',
193 MANAGE_CHANNELS: 'Créer, modifier et supprimer des salons',
194 KICK_MEMBERS: 'Retirer des membres du serveur',
195 BAN_MEMBERS: 'Bannir des membres du serveur',
196 SEND_MESSAGES: 'Envoyer des messages dans les salons textuels',
197 MANAGE_MESSAGES: 'Supprimer les messages envoyés par d\'autres membres',
198 MENTION_EVERYONE: 'Utiliser les mentions @everyone et @here',
199 CONNECT: 'Se connecter aux salons vocaux',
200 SPEAK: 'Parler dans les salons vocaux',
201 MUTE_MEMBERS: 'Rendre muets des membres dans les salons vocaux',
202 DEAFEN_MEMBERS: 'Couper le son de membres dans les salons vocaux',
203 // Ajoute d'autres descriptions...
204 };
205
206 return descriptions[permission] || 'Description de la permission';
207}La liste trie les rôles par position décroissante, donc le plus important est en haut, et la pastille à côté du nom a la couleur du rôle. La case d'une permission est cochée quand (selectedRole.permissions & bit) === bit, exactement le même test que dans hasPermission. Les champs du formulaire n'ont pas encore de onChange : pour l'instant c'est un aperçu, et l'enregistrement reviendra à onEditRole et au serveur.
Les outils de modération
Le type ModerationAction couvre les actions de modération kick, ban, timeout et warn, ainsi que unban pour annuler un bannissement :
1interface ModerationAction {
2 id: string;
3 type: 'kick' | 'ban' | 'timeout' | 'warn' | 'unban';
4 targetUserId: string;
5 moderatorId: string;
6 reason: string;
7 duration?: number; // pour timeout/ban (secondes)
8 createdAt: Date;
9}
10
11interface ModeratorToolsProps {
12 server: Server;
13 targetMember: Member;
14 currentUserId: string;
15 onKick: (reason: string) => void;
16 onBan: (reason: string, duration?: number) => void;
17 onTimeout: (duration: number, reason: string) => void;
18}
19
20export default function ModeratorTools({
21 server,
22 targetMember,
23 currentUserId,
24 onKick,
25 onBan,
26 onTimeout
27}: ModeratorToolsProps) {
28 const [action, setAction] = useState<'kick' | 'ban' | 'timeout' | null>(null);
29 const [reason, setReason] = useState('');
30 const [duration, setDuration] = useState(3600); // 1 heure par défaut
31
32 const currentMember = server.members.find(m => m.userId === currentUserId);
33 if (!currentMember) return null;
34
35 const canKick = hasPermission(currentMember, PermissionBits.KICK_MEMBERS, server.roles);
36 const canBan = hasPermission(currentMember, PermissionBits.BAN_MEMBERS, server.roles);
37
38 return (
39 <div className="bg-gray-800 rounded-lg p-4">
40 <h3 className="font-bold text-white mb-4">Actions de modération</h3>
41
42 <div className="space-y-2 mb-4">
43 {canKick && (
44 <button
45 onClick={() => setAction('kick')}
46 className="w-full py-2 bg-orange-600 hover:bg-orange-700 text-white rounded font-semibold"
47 >
48 Expulser du serveur
49 </button>
50 )}
51
52 {canBan && (
53 <>
54 <button
55 onClick={() => setAction('ban')}
56 className="w-full py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold"
57 >
58 Bannir du serveur
59 </button>
60
61 <button
62 onClick={() => setAction('timeout')}
63 className="w-full py-2 bg-yellow-600 hover:bg-yellow-700 text-white rounded font-semibold"
64 >
65 Exclusion temporaire
66 </button>
67 </>
68 )}
69 </div>
70
71 {action && (
72 <div className="border-t border-gray-700 pt-4">
73 <h4 className="text-white font-semibold mb-3">
74 {action === 'kick' && 'Expulser le membre'}
75 {action === 'ban' && 'Bannir le membre'}
76 {action === 'timeout' && 'Exclure temporairement le membre'}
77 </h4>
78
79 {action === 'timeout' && (
80 <div className="mb-3">
81 <label className="block text-sm text-gray-300 mb-2">
82 Durée
83 </label>
84 <select
85 value={duration}
86 onChange={(e) => setDuration(parseInt(e.target.value))}
87 className="w-full bg-gray-900 text-white px-3 py-2 rounded"
88 >
89 <option value={60}>60 secondes</option>
90 <option value={300}>5 minutes</option>
91 <option value={600}>10 minutes</option>
92 <option value={3600}>1 heure</option>
93 <option value={86400}>1 jour</option>
94 <option value={604800}>1 semaine</option>
95 </select>
96 </div>
97 )}
98
99 <div className="mb-3">
100 <label className="block text-sm text-gray-300 mb-2">
101 Raison
102 </label>
103 <textarea
104 value={reason}
105 onChange={(e) => setReason(e.target.value)}
106 className="w-full bg-gray-900 text-white px-3 py-2 rounded resize-none"
107 rows={3}
108 />
109 </div>
110
111 <div className="flex gap-2">
112 <button
113 onClick={() => {
114 if (action === 'kick') onKick(reason);
115 if (action === 'ban') onBan(reason);
116 if (action === 'timeout') onTimeout(duration, reason);
117 setAction(null);
118 setReason('');
119 }}
120 disabled={!reason.trim()}
121 className="flex-1 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-600 text-white rounded font-semibold"
122 >
123 Confirmer
124 </button>
125 <button
126 onClick={() => setAction(null)}
127 className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded"
128 >
129 Annuler
130 </button>
131 </div>
132 </div>
133 )}
134 </div>
135 );
136}Un kick retire un membre du serveur, mais il peut revenir avec une nouvelle invitation. Un ban empêche le retour. Un timeout retire temporairement le droit d'écrire et de parler ; les durées de la liste, de la plus courte, sont 60 secondes, 5 minutes, 10 minutes, 1 heure, 1 jour et 1 semaine, et Discord autorise au maximum 28 jours. Seul celui qui a la permission voit les boutons, car canKick et canBan sont calculés par hasPermission. La confirmation exige un motif : disabled={!reason.trim()}.
Conseil du Capitaine Barbe-Rouge : cacher un bouton n'est pas une protection. Le serveur doit vérifier hasPermission à nouveau à chaque expulsion et bannissement, car un pirate malin enverra la requête sans passer par ton interface.
À retenir : les permissions sont des bits additionnés à partir des rôles, et les surcharges de salon retirent d'abord, puis ajoutent. Pirate's Cove réunit maintenant ce que nous avons construit pas à pas : serveurs avec catégories et salons, salons vocaux avec présence enrichie, rôles avec modération. Dans l'éditeur ci-dessous, tu as le gestionnaire de rôles de l'équipage : choisis un rôle, regarde sa couleur et la liste des permissions à cocher, puis fais les exercices.
Code de cette leçon : App.jsx
1// Gestionnaire de rôles
2import { useState } from 'react';
3
4const roles = [
5 { id: 1, name: 'Captain', color: '#e74c3c', memberCount: 3, position: 4 },
6 { id: 2, name: 'First Mate', color: '#3498db', memberCount: 5, position: 3 },
7 { id: 3, name: 'Navigator', color: '#2ecc71', memberCount: 8, position: 2 },
8 { id: 4, name: 'Crew', color: '#95a5a6', memberCount: 45, position: 1 }
9];
10
11const permissions = [
12 'ADMINISTRATOR',
13 'MANAGE_SERVER',
14 'MANAGE_ROLES',
15 'MANAGE_CHANNELS',
16 'KICK_MEMBERS',
17 'BAN_MEMBERS',
18 'SEND_MESSAGES',
19 'MANAGE_MESSAGES'
20];
21
22export default function App() {
23 const [selectedRoleId, setSelectedRoleId] = useState(1);
24
25 const selectedRole = roles.find(r => r.id === selectedRoleId);
26
27 return (
28 <div className="flex h-screen bg-gray-800">
29 {/* Liste des rôles */}
30 <div className="w-64 bg-gray-900 p-4">
31 <div className="flex items-center justify-between mb-4">
32 <h3 className="font-bold text-white">Rôles</h3>
33 <button className="w-8 h-8 bg-green-600 hover:bg-green-700 rounded-full text-white">
34 +
35 </button>
36 </div>
37
38 <div className="space-y-1">
39 {roles.sort((a, b) => b.position - a.position).map(role => (
40 <button
41 key={role.id}
42 onClick={() => setSelectedRoleId(role.id)}
43 className={`w-full text-left px-3 py-2 rounded flex items-center justify-between ${
44 selectedRoleId === role.id ? 'bg-gray-700' : 'hover:bg-gray-800'
45 }`}
46 >
47 <div className="flex items-center gap-2 flex-1">
48 <div
49 className="w-3 h-3 rounded-full"
50 style={{ backgroundColor: role.color }}
51 />
52 <span className="text-white text-sm truncate">{role.name}</span>
53 </div>
54 <span className="text-xs text-gray-400">{role.memberCount}</span>
55 </button>
56 ))}
57 </div>
58 </div>
59
60 {/* Éditeur de rôle */}
61 {selectedRole && (
62 <div className="flex-1 p-6 overflow-y-auto">
63 <h2 className="text-2xl font-bold text-white mb-6">
64 Modifier le rôle : {selectedRole.name}
65 </h2>
66
67 {/* Affichage */}
68 <div className="bg-gray-700 rounded-lg p-4 mb-6">
69 <h3 className="font-bold text-white mb-3">Affichage</h3>
70
71 <div className="mb-4">
72 <label className="block text-sm text-gray-300 mb-2">Nom du rôle</label>
73 <input
74 type="text"
75 key={selectedRole.id}
76 defaultValue={selectedRole.name}
77 className="w-full bg-gray-900 text-white px-3 py-2 rounded"
78 />
79 </div>
80
81 <div className="mb-4">
82 <label className="block text-sm text-gray-300 mb-2">Couleur du rôle</label>
83 <div className="flex items-center gap-3">
84 <input
85 type="color"
86 key={selectedRole.id}
87 defaultValue={selectedRole.color}
88 className="w-16 h-10 rounded cursor-pointer"
89 />
90 <span className="text-gray-400 text-sm">{selectedRole.color}</span>
91 </div>
92 </div>
93
94 <label className="flex items-center gap-2 text-sm text-gray-300 mb-2">
95 <input type="checkbox" className="w-4 h-4" />
96 Afficher les membres du rôle séparément
97 </label>
98
99 <label className="flex items-center gap-2 text-sm text-gray-300">
100 <input type="checkbox" className="w-4 h-4" />
101 Autoriser tout le monde à @mentionner ce rôle
102 </label>
103 </div>
104
105 {/* Permissions */}
106 <div className="bg-gray-700 rounded-lg p-4">
107 <h3 className="font-bold text-white mb-3">Permissions</h3>
108
109 <div className="space-y-2">
110 {permissions.map(permission => (
111 <label
112 key={permission}
113 className="flex items-center gap-3 text-sm text-gray-300 hover:bg-gray-600 p-2 rounded cursor-pointer"
114 >
115 <input type="checkbox" className="w-4 h-4" />
116 <span>{permission.split('_').join(' ')}</span>
117 </label>
118 ))}
119 </div>
120 </div>
121
122 {/* Actions */}
123 <div className="mt-6 flex gap-3">
124 <button className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-semibold">
125 Enregistrer les modifications
126 </button>
127 <button className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded font-semibold">
128 Supprimer le rôle
129 </button>
130 </div>
131 </div>
132 )}
133 </div>
134 );
135}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment fonctionnent les permissions sur Discord ?
2. Comment une valeur de permission, par exemple KICK_MEMBERS, est-elle définie dans l'enum PermissionBits ?
Voici 2 des 8 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Clique dans l'ordre
Clique pour construire la valeur du bit de permission : ADMINISTRATOR = 1 << 3
- Éditeur de code
Interface de gestion des rôles
- Tri horizontal
Ordonne la hiérarchie des rôles (du plus haut) :
- Tri vertical
Ordonne les étapes du calcul des permissions dans la fonction hasPermission :
- Clique dans l'ordre
Clique pour construire l'ajout de la permission d'un rôle à la variable permissions :
- Tri horizontal
Ordonne la vérification qu'un membre possède un rôle donné : member.roles.includes(role.id)
- Tri vertical
Ordonne les durées de timeout disponibles (de la plus courte) :
- Éditeur de code
Panneau de modération
- Tri vertical
Ordonne les étapes du fonctionnement de la fonction getHighestRole :
- Tri horizontal
Ordonne les arguments de l'appel : hasPermission(member, KICK_MEMBERS, roles)
- Clique dans l'ordre
Clique pour construire la vérification qu'une permission est activée : (permissions & permission) === permission