Cours Vibe coding avec l'IA · Module 9: Clone de Discord

Rôles, permissions et modération

10 min de lecture
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 limite

Notre 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. 1. Comment fonctionnent les permissions sur Discord ?

  2. 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

Articles utiles