Playergency to społeczność graczy, wcześniej znana jako eRPe, działająca od 2016 roku. Nie jest moja: od 2017 roku tworzę dla niej bota i stronę. W październiku 2024 jej serwer Discord został zaatakowany: członkowie byli masowo wyrzucani i banowani. Najbardziej prawdopodobna przyczyna to przejęte lub zainfekowane publiczne boty, które miały na serwerze uprawnienia moderatora.
Skala i brak przycisku
Po wszystkim na liście banów byli praktycznie wszyscy. Discord nie miał wtedy opcji „odbanuj wszystkich”, a wsparcie Discorda nie pomogło. Zostało klikanie: jeden użytkownik, menu, „Cofnij bana”, potwierdzenie, następny. Przy całej społeczności na liście banów to monotonna praca i gwarancja pomyłek.
Zamiast klikać, napisałem w discord.js automat, który przeszedł przez całą listę banów i zdjął je jeden po drugim.
Jak by to dziś wyglądało
Oryginalny kod się nie zachował. Poniżej rekonstrukcja: tak bym to napisał dzisiaj, z tymi samymi założeniami. To nie jest kod z 2024 roku.
// Rekonstrukcja (2026), nie oryginalny kod z 2024 roku.
import { Client, Events, GatewayIntentBits } from 'discord.js';
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async () => {
const guild = await client.guilds.fetch(process.env.GUILD_ID!);
let after: string | undefined;
let lifted = 0;
// Discord zwraca bany stronami po maksymalnie 1000, posortowane po ID użytkownika.
for (;;) {
const page = await guild.bans.fetch({ limit: 1000, after, cache: false });
if (page.size === 0) break;
for (const ban of page.values()) {
// Celowo jeden po drugim: discord.js kolejkuje żądania per trasa
// i sam czeka po odpowiedzi 429, więc pętla nie wpada w limit.
await guild.bans.remove(ban.user.id, 'Mass unban after the raid');
lifted += 1;
if (lifted % 100 === 0) console.log(`lifted ${lifted}`);
}
after = page.lastKey();
}
console.log(`done, lifted ${lifted}`);
await client.destroy();
});
await client.login(process.env.DISCORD_TOKEN);
Trzy rzeczy warte uwagi w tym kodzie.
Stronicowanie. Lista banów nie przychodzi w całości. Endpoint zwraca najwyżej tysiąc wpisów, a kolejną stronę pobiera się parametrem after z ID ostatniego użytkownika. Kursor to ID, a nie numer strony, więc zdejmowanie banów w trakcie przeglądania niczego nie przesuwa.
Limity zapytań. Pokusa jest taka, żeby wrzucić wszystkie żądania naraz w Promise.all. Discord ma limity per trasa i dla całego bota, a po ich przekroczeniu odpowiada kodem 429. discord.js ma wbudowaną kolejkę, która czeka tyle, ile każe nagłówek odpowiedzi, ale równoległe żądania i tak trafiają w ten sam limit, więc nic się nie zyskuje, a w logach robi się bałagan. Pętla wykonująca żądania po kolei idzie tak szybko, jak pozwala Discord, i nic więcej tu nie potrzeba.
Powód w audycie. Drugi argument remove trafia do dziennika audytu serwera. Po takim incydencie moderatorzy powinni móc odróżnić bany cofnięte automatem od decyzji podjętych ręcznie.
Gdybym robił to dziś, dodałbym jeszcze jeden krok: zamiast zdejmować wszystkie bany, pobrałbym z dziennika audytu (AuditLogEvent.MemberBanAdd) te, które nałożyło konkretne, przejęte konto bota, i cofnąłbym tylko je. Wtedy bany nałożone wcześniej świadomie przez moderatorów zostają na miejscu.
Koszt, którego nie cofnie żaden skrypt
Automat zdjął bany, ale nie przywrócił ludzi. Osoba wyrzucona z serwera musi sama wrócić przez zaproszenie, a mniej więcej połowa nigdy tego nie zrobiła. Techniczne odtworzenie stanu okazało się tylko częścią odpowiedzi na incydent. Zaufania i nawyku zaglądania na serwer nie da się przywrócić pętlą.
Co bym doradził każdemu, kto prowadzi serwer Discord
To nie jest lista tego, co zostało zrobione po incydencie, tylko rady, które z niego wynikają:
- Najmniejsze możliwe uprawnienia dla botów. Publiczny bot z prawem do banowania to klucz do serwera w cudzej kieszeni. Jeśli bot nie musi moderować, nie powinien móc.
- Przegląd botów raz na jakiś czas. Każdy dodany bot to zależność, której kodu i bezpieczeństwa nie kontrolujesz. Te, których nikt już nie używa, najlepiej usunąć.
- Miej narzędzie na najgorszy dzień, zanim nadejdzie. Skrypt do masowego cofania działań, przetestowany na serwerze testowym, to kilkadziesiąt linii kodu. Pisanie go w trakcie incydentu to stres, którego da się uniknąć.